From cf110bc4c8b0713291174ac7cfb3730d0ff226a7 Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Fri, 30 Apr 2021 14:08:14 -0400 Subject: feat: Add the .mosaic command --- bot/exts/evergreen/avatar_modification/_effects.py | 158 ++++++++++++++++++++- .../evergreen/avatar_modification/avatar_modify.py | 50 ++++++- 2 files changed, 202 insertions(+), 6 deletions(-) (limited to 'bot/exts/evergreen') diff --git a/bot/exts/evergreen/avatar_modification/_effects.py b/bot/exts/evergreen/avatar_modification/_effects.py index e415d700..d2370b4b 100644 --- a/bot/exts/evergreen/avatar_modification/_effects.py +++ b/bot/exts/evergreen/avatar_modification/_effects.py @@ -1,3 +1,5 @@ +import math +import random import typing as t from io import BytesIO from pathlib import Path @@ -51,7 +53,7 @@ class PfpEffects: return r, g, b @staticmethod - def crop_avatar_circle(avatar: Image) -> Image: + 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) @@ -60,7 +62,7 @@ class PfpEffects: return avatar @staticmethod - def crop_ring(ring: Image, px: int) -> Image: + 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) @@ -70,7 +72,7 @@ class PfpEffects: return ring @staticmethod - def pridify_effect(image: Image, pixels: int, flag: str) -> Image: + def pridify_effect(image: Image.Image, pixels: int, flag: str) -> Image.Image: """Applies the given pride effect to the given image.""" image = image.resize((1024, 1024)) image = PfpEffects.crop_avatar_circle(image) @@ -83,7 +85,7 @@ class PfpEffects: return image @staticmethod - def eight_bitify_effect(image: Image) -> Image: + def eight_bitify_effect(image: Image.Image) -> Image.Image: """ Applies the 8bit effect to the given image. @@ -95,7 +97,7 @@ class PfpEffects: return image.quantize() @staticmethod - def easterify_effect(image: Image, overlay_image: Image = None) -> Image: + def easterify_effect(image: Image.Image, overlay_image: t.Optional[Image.Image] = None) -> Image.Image: """ Applies the easter effect to the given image. @@ -137,3 +139,149 @@ class PfpEffects: (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: t.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(img_bytes: bytes, squares: int, file_name: str) -> discord.File: + """Seperate function run from an executor which turns an image into a mosaic.""" + avatar = Image.open(BytesIO(img_bytes)) + avatar = avatar.convert('RGBA').resize((1024, 1024)) + + img_squares = PfpEffects.split_image(avatar, squares) + new_img = PfpEffects.join_images(img_squares) + + bufferedio = BytesIO() + new_img.save(bufferedio, format='PNG') + bufferedio.seek(0) + + return discord.File(bufferedio, filename=file_name) diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py index 0baee8b2..afff125f 100644 --- a/bot/exts/evergreen/avatar_modification/avatar_modify.py +++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py @@ -1,6 +1,7 @@ import asyncio import json import logging +import math import string import typing as t import unicodedata @@ -22,11 +23,15 @@ _EXECUTOR = ThreadPoolExecutor(10) FILENAME_STRING = "{effect}_{author}.png" +MAX_SQUARES = 10_000 + +T = t.TypeVar("T") + with open("bot/resources/pride/gender_options.json") as f: GENDER_OPTIONS = json.load(f) -async def in_executor(func: t.Callable, *args) -> t.Any: +async def in_executor(func: t.Callable[..., T], *args) -> T: """ Runs the given synchronus function `func` in an executor. @@ -308,6 +313,49 @@ class AvatarModify(commands.Cog): await ctx.send(file=file, embed=embed) + @avatar_modify.command(name='mosaic', root_aliases=('mosaic',)) + async def mosaic_command(self, ctx: commands.Context, squares: int = 16) -> None: + """Splits your avatar into x squares, randomizes them and stitches them back into a new image!""" + async with ctx.typing(): + if squares < 1: + raise commands.BadArgument("Squares must be a positive number") + + if not math.sqrt(squares).is_integer(): + raise commands.BadArgument("Squares must be a perfect square") + + if squares > MAX_SQUARES: + raise commands.BadArgument(f"Number of squares cannot be higher than {MAX_SQUARES:,}.") + + file_name = file_safe_name("mosaic_avatar", ctx.author.display_name) + img_bytes = await ctx.author.avatar_url.read() + + file = await in_executor( + PfpEffects.mosaic_effect, + img_bytes, + squares, + file_name + ) + + if squares == 1: + title = 'Hooh... that was a lot of work' + description = 'I present to you... Yourself!' + elif squares == MAX_SQUARES: + title = 'Testing the limits I see...' + description = 'What a masterpiece. :star:' + else: + title = 'Your mosaic avatar' + description = 'Here is your avatar. I think it looks a bit *puzzling*' + + embed = discord.Embed( + title=title, + description=description + ) + + embed.set_image(url=f'attachment://{file_name}') + embed.set_footer(text=f'Made by {ctx.author.display_name}', icon_url=ctx.author.avatar_url) + + await ctx.send(file=file, embed=embed) + def setup(bot: commands.Bot) -> None: """Load the PfpModify cog.""" -- cgit v1.2.3 From ccb7cd7bc4ab43e4d777093a14c921a64c884f6e Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Wed, 5 May 2021 11:28:01 -0400 Subject: chore: Add a 5 second cooldown per user to .catify --- bot/exts/evergreen/catify.py | 1 + 1 file changed, 1 insertion(+) (limited to 'bot/exts/evergreen') diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py index d8a7442d..a175602f 100644 --- a/bot/exts/evergreen/catify.py +++ b/bot/exts/evergreen/catify.py @@ -16,6 +16,7 @@ class Catify(commands.Cog): self.bot = bot @commands.command(aliases=["ᓚᘏᗢify", "ᓚᘏᗢ"]) + @commands.cooldown(1, 5, commands.BucketType.user) async def catify(self, ctx: commands.Context, *, text: Optional[str]) -> None: """ Convert the provided text into a cat themed sentence by interspercing cats throughout text. -- cgit v1.2.3 From 544a05b00758583f4594569e032c9e661406a72f Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Wed, 5 May 2021 16:50:59 -0400 Subject: chore: Fetch the member and use Colours.blue in the embed for the .mosaic command --- bot/exts/evergreen/avatar_modification/avatar_modify.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'bot/exts/evergreen') diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py index afff125f..bd151d30 100644 --- a/bot/exts/evergreen/avatar_modification/avatar_modify.py +++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py @@ -317,6 +317,11 @@ class AvatarModify(commands.Cog): async def mosaic_command(self, ctx: commands.Context, squares: int = 16) -> None: """Splits your avatar into x squares, randomizes them and stitches them back into a new image!""" async with ctx.typing(): + member = await self._fetch_member(ctx.author.id) + if not member: + await ctx.send(f"{Emojis.cross_mark} Could not get member info.") + return + if squares < 1: raise commands.BadArgument("Squares must be a positive number") @@ -327,7 +332,8 @@ class AvatarModify(commands.Cog): raise commands.BadArgument(f"Number of squares cannot be higher than {MAX_SQUARES:,}.") file_name = file_safe_name("mosaic_avatar", ctx.author.display_name) - img_bytes = await ctx.author.avatar_url.read() + + img_bytes = await member.avatar_url.read() file = await in_executor( PfpEffects.mosaic_effect, @@ -348,7 +354,8 @@ class AvatarModify(commands.Cog): embed = discord.Embed( title=title, - description=description + description=description, + colour=Colours.blue ) embed.set_image(url=f'attachment://{file_name}') -- cgit v1.2.3 From 390c05383e42ba60197f5fb888cd93e867c00038 Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Wed, 5 May 2021 17:17:08 -0400 Subject: chore: Prefer double quotes over single quotes --- .../evergreen/avatar_modification/avatar_modify.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'bot/exts/evergreen') diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py index bd151d30..ca048134 100644 --- a/bot/exts/evergreen/avatar_modification/avatar_modify.py +++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py @@ -313,7 +313,7 @@ class AvatarModify(commands.Cog): await ctx.send(file=file, embed=embed) - @avatar_modify.command(name='mosaic', root_aliases=('mosaic',)) + @avatar_modify.command(name="mosaic", root_aliases=("mosaic",)) async def mosaic_command(self, ctx: commands.Context, squares: int = 16) -> None: """Splits your avatar into x squares, randomizes them and stitches them back into a new image!""" async with ctx.typing(): @@ -343,14 +343,14 @@ class AvatarModify(commands.Cog): ) if squares == 1: - title = 'Hooh... that was a lot of work' - description = 'I present to you... Yourself!' + title = "Hooh... that was a lot of work" + description = "I present to you... Yourself!" elif squares == MAX_SQUARES: - title = 'Testing the limits I see...' - description = 'What a masterpiece. :star:' + title = "Testing the limits I see..." + description = "What a masterpiece. :star:" else: - title = 'Your mosaic avatar' - description = 'Here is your avatar. I think it looks a bit *puzzling*' + title = "Your mosaic avatar" + description = "Here is your avatar. I think it looks a bit *puzzling*" embed = discord.Embed( title=title, @@ -358,8 +358,8 @@ class AvatarModify(commands.Cog): colour=Colours.blue ) - embed.set_image(url=f'attachment://{file_name}') - embed.set_footer(text=f'Made by {ctx.author.display_name}', icon_url=ctx.author.avatar_url) + embed.set_image(url=f"attachment://{file_name}") + embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) await ctx.send(file=file, embed=embed) -- cgit v1.2.3 From f6175b5cf842c7052089c05f387770fc9d91b6d0 Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Wed, 5 May 2021 17:19:32 -0400 Subject: chore: Use the name AvatarModify instead of PfpModify --- bot/exts/evergreen/avatar_modification/avatar_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/evergreen') diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py index ca048134..221bd809 100644 --- a/bot/exts/evergreen/avatar_modification/avatar_modify.py +++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py @@ -365,5 +365,5 @@ class AvatarModify(commands.Cog): def setup(bot: commands.Bot) -> None: - """Load the PfpModify cog.""" + """Load the AvatarModify cog.""" bot.add_cog(AvatarModify(bot)) -- cgit v1.2.3 From 16b43597050b8dad922c8d93aa07c061e0512c1f Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Thu, 6 May 2021 13:05:37 -0400 Subject: chore: Check if the number of squares first is bigger than the max first Co-authored-by: Anand Krishna <40204976+anand2312@users.noreply.github.com> --- bot/exts/evergreen/avatar_modification/avatar_modify.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'bot/exts/evergreen') diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py index 221bd809..dffe43ce 100644 --- a/bot/exts/evergreen/avatar_modification/avatar_modify.py +++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py @@ -322,14 +322,11 @@ class AvatarModify(commands.Cog): await ctx.send(f"{Emojis.cross_mark} Could not get member info.") return - if squares < 1: - raise commands.BadArgument("Squares must be a positive number") + if 1 <= squares <= MAX_SQUARES: + raise commands.BadArgument(f"Squares must be a positive number less than or equal to {MAX_SQUARES:,}.") if not math.sqrt(squares).is_integer(): - raise commands.BadArgument("Squares must be a perfect square") - - if squares > MAX_SQUARES: - raise commands.BadArgument(f"Number of squares cannot be higher than {MAX_SQUARES:,}.") + raise commands.BadArgument("The number of squares must be a perfect square.") file_name = file_safe_name("mosaic_avatar", ctx.author.display_name) -- cgit v1.2.3 From 40d5a00f1b609b23a6cd77a3b7f1e4814ed7df82 Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Fri, 7 May 2021 11:01:24 -0400 Subject: chore: Get the next perfect square If the amount of squares is not a perfect square, get the next highest perfect square --- bot/exts/evergreen/avatar_modification/avatar_modify.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'bot/exts/evergreen') diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py index dffe43ce..6418eaee 100644 --- a/bot/exts/evergreen/avatar_modification/avatar_modify.py +++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py @@ -325,8 +325,10 @@ class AvatarModify(commands.Cog): if 1 <= squares <= MAX_SQUARES: raise commands.BadArgument(f"Squares must be a positive number less than or equal to {MAX_SQUARES:,}.") - if not math.sqrt(squares).is_integer(): - raise commands.BadArgument("The number of squares must be a perfect square.") + sqrt = math.sqrt(squares) + + if not sqrt.is_integer(): + squares = math.ceil(sqrt) ** 2 # Get the next perfect square file_name = file_safe_name("mosaic_avatar", ctx.author.display_name) -- cgit v1.2.3 From 14fc010db6a8ac06eb356617146e9deaa540177b Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Fri, 7 May 2021 11:34:35 -0400 Subject: fix: Add a missing 'not' --- bot/exts/evergreen/avatar_modification/avatar_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/evergreen') diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py index 6418eaee..2afc3b74 100644 --- a/bot/exts/evergreen/avatar_modification/avatar_modify.py +++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py @@ -322,7 +322,7 @@ class AvatarModify(commands.Cog): await ctx.send(f"{Emojis.cross_mark} Could not get member info.") return - if 1 <= squares <= MAX_SQUARES: + if not 1 <= squares <= MAX_SQUARES: raise commands.BadArgument(f"Squares must be a positive number less than or equal to {MAX_SQUARES:,}.") sqrt = math.sqrt(squares) -- cgit v1.2.3