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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
|
import math
import random
from io import BytesIO
from pathlib import Path
from typing import Callable, Optional
import discord
from PIL import Image, ImageDraw, ImageOps
from bot.constants import Colours
class PfpEffects:
"""
Implements various image modifying effects, for the PfpModify cog.
All of these functions are slow, and blocking, so they should be ran in executors.
"""
@staticmethod
def apply_effect(image_bytes: bytes, effect: Callable, filename: str, *args) -> discord.File:
"""Applies the given effect to the image passed to it."""
im = Image.open(BytesIO(image_bytes))
im = im.convert("RGBA")
im = im.resize((1024, 1024))
im = effect(im, *args)
bufferedio = BytesIO()
im.save(bufferedio, format="PNG")
bufferedio.seek(0)
return discord.File(bufferedio, filename=filename)
@staticmethod
def closest(x: tuple[int, int, int]) -> tuple[int, int, int]:
"""
Finds the closest "easter" colour to a given pixel.
Returns a merge between the original colour and the closest colour.
"""
r1, g1, b1 = x
def distance(point: tuple[int, int, int]) -> int:
"""Finds the difference between a pastel colour and the original pixel colour."""
r2, g2, b2 = point
return (r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2
closest_colours = sorted(Colours.easter_like_colours, key=distance)
r2, g2, b2 = closest_colours[0]
r = (r1 + r2) // 2
g = (g1 + g2) // 2
b = (b1 + b2) // 2
return r, g, b
@staticmethod
def crop_avatar_circle(avatar: Image.Image) -> Image.Image:
"""This crops the avatar given into a circle."""
mask = Image.new("L", avatar.size, 0)
draw = ImageDraw.Draw(mask)
draw.ellipse((0, 0) + avatar.size, fill=255)
avatar.putalpha(mask)
return avatar
@staticmethod
def crop_ring(ring: Image.Image, px: int) -> Image.Image:
"""This crops the given ring into a circle."""
mask = Image.new("L", ring.size, 0)
draw = ImageDraw.Draw(mask)
draw.ellipse((0, 0) + ring.size, fill=255)
draw.ellipse((px, px, 1024-px, 1024-px), fill=0)
ring.putalpha(mask)
return ring
@staticmethod
def pridify_effect(image: Image.Image, pixels: int, flag: str) -> Image.Image:
"""Applies the given pride effect to the given image."""
image = PfpEffects.crop_avatar_circle(image)
ring = Image.open(Path(f"bot/resources/holidays/pride/flags/{flag}.png")).resize((1024, 1024))
ring = ring.convert("RGBA")
ring = PfpEffects.crop_ring(ring, pixels)
image.alpha_composite(ring, (0, 0))
return image
@staticmethod
def eight_bitify_effect(image: Image.Image) -> Image.Image:
"""
Applies the 8bit effect to the given image.
This is done by reducing the image to 32x32 and then back up to 1024x1024.
We then quantize the image before returning too.
"""
image = image.resize((32, 32), resample=Image.NEAREST)
image = image.resize((1024, 1024), resample=Image.NEAREST)
return image.quantize()
@staticmethod
def flip_effect(image: Image.Image) -> Image.Image:
"""
Flips the image horizontally.
This is done by just using ImageOps.mirror().
"""
image = ImageOps.mirror(image)
return image
@staticmethod
def easterify_effect(image: Image.Image, overlay_image: Optional[Image.Image] = None) -> Image.Image:
"""
Applies the easter effect to the given image.
This is done by getting the closest "easter" colour to each pixel and changing the colour
to the half-way RGB value.
We also then add an overlay image on top in middle right, a chocolate bunny by default.
"""
if overlay_image:
ratio = 64 / overlay_image.height
overlay_image = overlay_image.resize((
round(overlay_image.width * ratio),
round(overlay_image.height * ratio)
))
overlay_image = overlay_image.convert("RGBA")
else:
overlay_image = Image.open(Path("bot/resources/holidays/easter/chocolate_bunny.png"))
alpha = image.getchannel("A").getdata()
image = image.convert("RGB")
image = ImageOps.posterize(image, 6)
data = image.getdata()
data_set = set(data)
easterified_data_set = {}
for x in data_set:
easterified_data_set[x] = PfpEffects.closest(x)
new_pixel_data = [
(*easterified_data_set[x], alpha[i])
if x in easterified_data_set else x
for i, x in enumerate(data)
]
im = Image.new("RGBA", image.size)
im.putdata(new_pixel_data)
im.alpha_composite(
overlay_image,
(im.width - overlay_image.width, (im.height - overlay_image.height) // 2)
)
return im
@staticmethod
def split_image(img: Image.Image, squares: int) -> list:
"""
Split an image into a selection of squares, specified by the squares argument.
Explanation:
1. It gets the width and the height of the Image passed to the function.
2. It gets the root of a number of squares (number of squares) passed, which is called xy. Reason: if let's say
25 squares (number of squares) were passed, that is the total squares (split pieces) that the image is supposed
to be split into. As it is known, a 2D shape has a height and a width, and in this case the program thinks of it
as rows and columns. Rows multiplied by columns is equal to the passed squares (number of squares). To get rows
and columns, since in this case, each square (split piece) is identical, rows are equal to columns and the
program treats the image as a square-shaped, it gets the root out of the squares (number of squares) passed.
3. Now width and height are both of the original Image, Discord PFP, so when it comes to forming the squares,
the program divides the original image's height and width by the xy. In a case of 25 squares (number of squares)
passed, xy would be 5, so if an image was 250x300, x_frac would be 50 and y_frac - 60. Note:
x_frac stands for a fracture of width. The reason it's called that is because it is shorter to use x for width
in mind and then it's just half of the word fracture, same applies to y_frac, just height instead of width.
x_frac and y_frac are width and height of a single square (split piece).
4. With left, top, right, bottom, = 0, 0, x_frac, y_frac, the program sets these variables to create the initial
square (split piece). Explanation: all of these 4 variables start at the top left corner of the Image, by adding
value to right and bottom, it's creating the initial square (split piece).
5. In the for loop, it keeps adding those squares (split pieces) in a row and once (index + 1) % xy == 0 is
True, it adds to top and bottom to lower them and reset right and left to recreate the initial space between
them, forming a square (split piece), it also adds the newly created square (split piece) into the new_imgs list
where it stores them. The program keeps repeating this process till all 25 squares get added to the list.
6. It returns new_imgs, a list of squares (split pieces).
"""
width, heigth = img.size
xy = math.sqrt(squares)
x_frac = width // xy
y_frac = heigth // xy
left, top, right, bottom, = 0, 0, x_frac, y_frac
new_imgs = []
for index in range(squares):
new_img = img.crop((left, top, right, bottom))
new_imgs.append(new_img)
if (index + 1) % xy == 0:
top += y_frac
bottom += y_frac
left = 0
right = x_frac
else:
left += x_frac
right += x_frac
return new_imgs
@staticmethod
def join_images(images: list[Image.Image]) -> Image.Image:
"""
Stitches all the image squares into a new image.
Explanation:
1. Shuffles the passed images to randomize the pieces.
2. The program gets a single square (split piece) out of the list and defines single_width as the square's width
and single_height as the square's height.
3. It gets the root of type integer of the number of images (split pieces) in the list and calls it multiplier.
Program then proceeds to calculate total height and width of the new image that it's creating using the same
multiplier.
4. The program then defines new_image as the image that it's creating, using the previously obtained total_width
and total_height.
5. Now it defines width_multiplier as well as height with values of 0. These will be used to correctly position
squares (split pieces) onto the new_image canvas.
6. Similar to how in the split_image function, the program gets the root of number of images in the list.
In split_image function, it was the passed squares (number of squares) instead of a number of imgs in the
list that it got the square of here.
7. In the for loop, as it iterates, the program multiplies single_width by width_multiplier to correctly
position a square (split piece) width wise. It then proceeds to paste the newly positioned square (split piece)
onto the new_image. The program increases the width_multiplier by 1 every iteration so the image wouldn't get
pasted in the same spot and the positioning would move accordingly. It makes sure to increase the
width_multiplier before the check, which checks if the end of a row has been reached, -
(index + 1) % pieces == 0, so after it, if it was True, width_multiplier would have been reset to 0 (start of
the row). If the check returns True, the height gets increased by a single square's (split piece) height to
lower the positioning height wise and, as mentioned, the width_multiplier gets reset to 0 and width will
then be calculated from the start of the new row. The for loop finishes once all the squares (split pieces) were
positioned accordingly.
8. Finally, it returns the new_image, the randomized squares (split pieces) stitched back into the format of the
original image - user's PFP.
"""
random.shuffle(images)
single_img = images[0]
single_wdith = single_img.size[0]
single_height = single_img.size[1]
multiplier = int(math.sqrt(len(images)))
total_width = multiplier * single_wdith
total_height = multiplier * single_height
new_image = Image.new("RGBA", (total_width, total_height), (250, 250, 250))
width_multiplier = 0
height = 0
squares = math.sqrt(len(images))
for index, image in enumerate(images):
width = single_wdith * width_multiplier
new_image.paste(image, (width, height))
width_multiplier += 1
if (index + 1) % squares == 0:
width_multiplier = 0
height += single_height
return new_image
@staticmethod
def mosaic_effect(image: Image.Image, squares: int) -> Image.Image:
"""
Applies a mosaic effect to the given image.
The "squares" argument specifies the number of squares to split
the image into. This should be a square number.
"""
img_squares = PfpEffects.split_image(image, squares)
new_img = PfpEffects.join_images(img_squares)
return new_img
|