diff options
Diffstat (limited to '')
| -rw-r--r-- | bot/exts/evergreen/avatar_modification/_effects.py | 158 | ||||
| -rw-r--r-- | bot/exts/evergreen/avatar_modification/avatar_modify.py | 58 | ||||
| -rw-r--r-- | bot/exts/evergreen/catify.py | 1 | 
3 files changed, 210 insertions, 7 deletions
| 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 2304e459..10e30bb2 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 @@ -23,11 +24,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. @@ -309,7 +314,56 @@ 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(): +            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 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) + +            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) + +            img_bytes = await member.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, +                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) + +            await ctx.send(file=file, embed=embed) +  def setup(bot: Bot) -> None: -    """Load the PfpModify cog.""" +    """Load the AvatarModify cog."""      bot.add_cog(AvatarModify(bot)) diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py index b4ae4a25..3182f69e 100644 --- a/bot/exts/evergreen/catify.py +++ b/bot/exts/evergreen/catify.py @@ -14,6 +14,7 @@ class Catify(commands.Cog):      """Cog for the catify command."""      @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. | 
