From 52801b4de1d63cd01191acf7fcee137977140ec0 Mon Sep 17 00:00:00 2001 From: lexicade Date: Fri, 27 Jan 2023 21:06:30 +0000 Subject: Project init --- cogs/Image.py | 622 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 622 insertions(+) create mode 100644 cogs/Image.py (limited to 'cogs/Image.py') diff --git a/cogs/Image.py b/cogs/Image.py new file mode 100644 index 0000000..aea7e22 --- /dev/null +++ b/cogs/Image.py @@ -0,0 +1,622 @@ +import math +import re + +from discord.ext import commands +import discord +import os +from io import BytesIO +from PIL import Image +from PIL import ImageFont +from PIL import ImageDraw +from PIL import ImageFilter +from PIL import ExifTags +import time +import mimetypes +import requests +import json +import importlib +import utils +import random +import traceback +importlib.reload(utils) + + +class ImageCmd(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command( + aliases=[], + application_command_meta=commands.ApplicationCommandMeta( + options=[ + discord.ApplicationCommandOption( + name="template_name", + description="The name of the template.", + type=discord.ApplicationCommandOptionType.string, + required=True, + ) + ], + ) + ) + @commands.defer(ephemeral=False) + @commands.bot_has_permissions(send_messages=True) + async def image(self, ctx, template_name): + """A meme creating command. Use /imagelist for available templates.""" + # Check to see if the guild has it's own image folder + if not os.path.exists(f"./overlays/{ctx.guild.id}"): + os.mkdir(f"./overlays/{ctx.guild.id}") + os.mkdir(f"./overlays/{ctx.guild.id}/images") + + templates = self.get_image_template_names_listed() + print(f"templates {templates}") + if template_name.casefold() not in templates: + await ctx.send(f"Image templates: {', '.join(templates)}") + return + + image = await self.get_image(ctx, "image") + print(f"image {image}") + if not image: + emb = await utils.embed(ctx, "No image found", "To use this command, an image must be within the 20 most recent messages.") + await ctx.send(embed=emb) + return + + overlay_return = await self.img_edit_overlay(image, template_name, ctx.guild.id, ctx) + file = await self.pil_to_discordfile(overlay_return) + await ctx.send(file=file) + + @classmethod + async def pil_to_discordfile(cls, file, filename='image.png'): + temp_image = BytesIO() + file.save(temp_image, 'PNG') + temp_image.seek(0) + return discord.File(fp=temp_image, filename=filename) + + @commands.defer(ephemeral=True) + @commands.command(aliases=[], application_command_meta=commands.ApplicationCommandMeta(options=[])) + async def imagelist(self, ctx): + """Show all available templates to use with /image""" + image_list = ImageList() + image_list.generate_image_list() + file = await image_list.get_image_for_discord() + await ctx.send(file=file) + + @commands.defer(ephemeral=False) + + @commands.command( + aliases=[], + application_command_meta=commands.ApplicationCommandMeta( + options=[ + discord.ApplicationCommandOption( + name="top_text", + description="Top text for this image.", + type=discord.ApplicationCommandOptionType.string, + required=False, + ), + discord.ApplicationCommandOption( + name="bottom_text", + description="Bottom text for this image.", + type=discord.ApplicationCommandOptionType.string, + required=False, + ) + ], + ) + ) + async def text(self, ctx, top_text="", bottom_text=""): + """Add a caption to the last image posted.""" + print(f"ctx {ctx}") + top_text = "" if not 'top_text' in ctx.given_values else ctx.given_values['top_text'] + bottom_text = "" if not 'bottom_text' in ctx.given_values else ctx.given_values['bottom_text'] + print(f"top_text {top_text}") + print(f"bottom_text {bottom_text}") + img = await self.get_image(ctx, "image") + try: + if img: + str_filename = await self.img_edit_add_text(img, top_text, bottom_text, ctx.guild.id) + await ctx.send(file=discord.File(open(str_filename, 'rb'), str_filename)) + + else: + emb = await utils.embed(ctx, f"Unable to retrieve image", "I wasn't able to see a image in the recent chat history.") + await ctx.send(embed=emb) + except Exception as e: + traceback.print_exc() + print(f"Error: {e}") + emb = await utils.embed(ctx, f"Error", e) + await ctx.send(embed=emb) + + @commands.defer(ephemeral=False) + @commands.command(aliases=[], application_command_meta=commands.ApplicationCommandMeta(options=[])) + async def deep(self, ctx): + """Send the last image posted through a Deep Dream""" + print(f"ctx {ctx}") + img = await self.get_image(ctx, "url") + emb = await self.deep_master(ctx, img) + await ctx.send(embed=emb) + + @commands.defer(ephemeral=False) + @commands.context_command(name="DeepAI - Deep Dream") + async def deep_context(self, ctx: discord.ext.commands.context.Context, message: discord.message.Message): + """Send the last image posted through a Deep Dream - Via Context Menu""" + if len(message.embeds) > 0 and message.embeds[0].image: + image = message.embeds[0].image.url + elif len(message.attachments) > 0: + image = message.attachments[0].url + else: + emb = await utils.embed(ctx, f"Unable to retrieve image", "I wasn't able to see an image in the message you selected.") + await ctx.send(embed=emb) + return + + emb = await self.deep_master(ctx, image) + await ctx.send(embed=emb) + + async def deep_master(self, ctx, img): + """Send the last image posted through a Deep Dream""" + start_time = time.time() + + if img: + response = requests.post("https://api.deepai.org/api/deepdream", + headers={'api-key': 'd1a66541-5c1f-4862-b011-46972d1287ee'}, + data={'image': img}) + return_data = json.loads(response.text) + + emb = await utils.embed(ctx, + title="Deep Dream", + message="", + footer=f"\nDone in {round(time.time() - start_time, 2)} seconds.", + image=return_data['output_url']) + else: + emb = await utils.embed(ctx, f"Unable to retrieve image", "I wasn't able to see a image in the recent chat history.") + return emb + + @commands.defer(ephemeral=False) + @commands.command(aliases=[], application_command_meta=commands.ApplicationCommandMeta(options=[])) + async def intensify(self, ctx, intensity: int = 1): + """Shake images with intensity and violence.""" + img = await self.get_image(ctx.channel, "image") + + img.paste(img, (0, 0)) + + if img: + img_x, img_y = img.size + + max_scale = 200 + + img_x = float(img_x / 100) + img_y = float(img_y / 100) + while img_x < max_scale or img_y < max_scale: + img_x = img_x + float(img_x / 100) + img_y = img_y + float(img_y / 100) + img_x = int(img_x) + img_y = int(img_y) + img = img.resize((img_x, img_y), Image.ANTIALIAS) + + frames = [] + for i in range(4): + img_x_nudge = random.randint(round(-(img_x / 100 * intensity)), (round(img_x / 100 * intensity))) + img_y_nudge = random.randint(round(-(img_y / 100 * intensity)), (round(img_y / 100 * intensity))) + + im = Image.new('RGBA', (img_x, img_y), (255, 255, 255, 0)) + # im.convert('RGBA') + im.paste(img, (img_x_nudge, img_y_nudge)) + # im.convert('RGBA') + + frames.append(im) + # im.save(f'test{i}.png') + + arr = BytesIO() + frames[0].save('test_gif1.gif', format='GIF', save_all=True, duration=30, loop=0, transparency=0, disposal=0) + frames[0].save('test_gif2.gif', format='GIF', save_all=True, duration=30, loop=0, transparency=255, disposal=0) + frames[0].save('test_gif3.gif', format='GIF', save_all=True, duration=30, loop=0, transparency=0, disposal=1) + frames[0].save('test_gif4.gif', format='GIF', save_all=True, duration=30, loop=0, transparency=255, disposal=1) + frames[0].save('test_gif5.gif', format='GIF', save_all=True, duration=30, loop=0, transparency=0, disposal=2) + frames[0].save('test_gif6.gif', format='GIF', save_all=True, duration=30, loop=0, transparency=255, disposal=2) + frames[0].save('intensify.gif', format='GIF', append_images=frames[1:], save_all=True, duration=30, loop=0, disposal=0) + # img.save(arr, format='GIF', append_images=frames[1:], save_all=True, duration=30, loop=0, transparency=0, disposal=2) + + img_debug = Image.new('RGBA', (img_x, img_y), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img_debug) + draw.ellipse((25, 25, 75, 75), fill=(255, 0, 0)) + img_debug.save(arr, format='GIF', transparency=0) + + # first_frame = frames.pop(0) + # first_frame.save("out.gif", save_all=True, append_images=frames, duration=30, loop=0, transparency=0, disposal=2) + + arr.seek(0) + await ctx.send(file=discord.File(arr, 'intensify.gif')) + return + else: + await ctx.send(content="Cannot find image.") + + async def get_image(self, channel: discord.TextChannel, return_as: str): + image = None + async for message in channel.history(limit=20, before=None, after=None): + re_matches = re.findall(r"(https?://\S+)", message.content) + + if len(message.embeds) > 0 and message.embeds[0].image: + image = message.embeds[0].image.url + break + + elif len(message.attachments) > 0: + image = message.attachments[0].url + break + + elif len(re_matches) > 0: + for match in re_matches: + if '?' in match: + match = match.split("?")[0] + + if match.endswith(('jpg', 'jpeg', 'png', 'gif',)): + image = match + break + if image: + break + + if not image: + return None + + if return_as == 'url': + return image + + elif return_as == 'image': + image = Image.open(BytesIO(requests.get(image).content)) + + if image.format in ['GIF']: + return image + else: + rotation = await self.get_exif_data(image) + if rotation: + image = image.rotate(rotation, expand=True) + return image + + @staticmethod + async def send_image(ctx, file, title, message): + + emb = await utils.embed(ctx, title=title, message=message) + await ctx.send(embed=emb, file=discord.File(file, file.name)) + + if ctx.channel.permissions_for(ctx.me).manage_messages: + await ctx.message.delete() + + @staticmethod + async def get_exif_data(image): + exif = image._getexif() + for orientation in ExifTags.TAGS.keys(): + if ExifTags.TAGS[orientation] == 'Orientation': + break + + print(f"orientation {orientation}") + + if exif and orientation and exif[orientation] == 3: + return 180 + elif exif and orientation and exif[orientation] == 6: + return 270 + elif exif and orientation and exif[orientation] == 8: + return 90 + else: + return None + + @commands.defer(ephemeral=False) + @commands.command(hidden=True) + async def emboss(self, ctx, intensity=1): + """Image Effect: Emboss""" + img = await self.get_image(ctx.channel, "image") + file = await self.img_edit_effect(img, ImageFilter.EMBOSS, ctx, intensity) + await ctx.send(file=file) + + @commands.defer(ephemeral=False) + @commands.command(hidden=True) + async def blur(self, ctx, intensity=1): + """Image Effect: Blur""" + img = await self.get_image(ctx.channel, "image") + file = await self.img_edit_effect(img, ImageFilter.BLUR, ctx, intensity) + await ctx.send(file=file) + + @commands.defer(ephemeral=False) + @commands.command(hidden=True) + async def sharpen(self, ctx, intensity=1): + """Image Effect: Sharpen""" + img = await self.get_image(ctx.channel, "image") + file = await self.img_edit_effect(img, ImageFilter.SHARPEN, ctx, intensity) + await ctx.send(file=file) + + @staticmethod + def is_url_image(url): + mimetype, encoding = mimetypes.guess_type(url) + return mimetype and mimetype.startswith('image') + + @staticmethod + def check_url(url): + try: + request = requests.get(url) + if request.status_code == 200: + return True + except Exception as e: + return False + + def is_image_and_ready(self, url): + return self.is_url_image(url) and self.check_url(url) + + @staticmethod + def get_image_template_names(guild_id): + template_files = [] + + for file in os.listdir(f"./overlays/{guild_id}/"): + if str(file).startswith("overlay"): + template_files.append(f"{file.replace('overlay_', '').split('.')[0]}") + elif str(file).startswith("frame"): + template_files.append(f"{file.replace('frame_', '').split('.')[0]}") + + return template_files + + @staticmethod + def get_image_template_names_listed(): + images = [] + + for file in os.listdir(f"./config/images/"): + if 'templated_image.png' in file: + continue + else: + images.append(f"{file.split('.')[0]}") + + # Return formatted lists + if len(images) > 0: + images.sort() + return images + else: + return [] + + async def img_edit_add_text(self, img, top_text: str, bottom_text: str, guild_id): + # Define images + str_filename = f"./overlays/{guild_id}/tmp_text.png" + + img = await self.img_edit_add_text_write(img, top_text, "top") + print(f"Success top") + img = await self.img_edit_add_text_write(img, bottom_text, "bottom") + print(f"Success bottom") + + # Save Image and return + img.save(str_filename) + return str_filename + + @staticmethod + async def img_edit_add_text_write(img, input_text, text_pos): + # Check font is less than image width + img_w, img_h = img.size + img_draw = ImageDraw.Draw(img) + int_font = 90 + font = ImageFont.truetype("./fonts/calibri/calibri.ttf", int_font) + text_w, text_h = img_draw.textsize(input_text, font=font) + + while text_w > img_w: + int_font = int_font - 1 + font = ImageFont.truetype("./fonts/calibri/calibri.ttf", int_font) + text_w, text_h = img_draw.textsize(input_text, font=font) + + # Set text + int_x = (img_w - text_w) / 2 + int_border = 3 + if text_pos == "top": + int_y = -5 + elif text_pos == "bottom": + int_y = img_h - text_h - 10 + + # Add Text diagonals + img_draw.text((int_x - int_border, int_y - int_border), input_text, font=font, fill="black") + img_draw.text((int_x + int_border, int_y - int_border), input_text, font=font, fill="black") + img_draw.text((int_x - int_border, int_y + int_border), input_text, font=font, fill="black") + img_draw.text((int_x + int_border, int_y + int_border), input_text, font=font, fill="black") + + # Add Text straights + img_draw.text((int_x + int_border, int_y), input_text, font=font, fill="black") + img_draw.text((int_x - int_border, int_y), input_text, font=font, fill="black") + img_draw.text((int_x, int_y + int_border), input_text, font=font, fill="black") + img_draw.text((int_x, int_y - int_border), input_text, font=font, fill="black") + img_draw.text((int_x, int_y), input_text, (255, 255, 255), font=font) # Inner Text + return img + + async def img_edit_overlay(self, template, overlay_name, guild_id, ctx): + image_overlay_type = 'overlay' + match_found = False + + # Define order for overlays + if image_overlay_type == 'overlay': + bg = Image.open(f"./config/images/{overlay_name}.png") + fg = template + + img = Image.new("RGBA", bg.size) + str_filename = f"./config/images/templated_image.png" + + im_boundry, im_topleft = self.get_image_transparency_boundry(f"./config/images/{overlay_name}.png") + + fg_scaled_x, fg_scaled_y = fg.size + fg_scaled_x = float(fg_scaled_x / 100) + fg_scaled_y = float(fg_scaled_y / 100) + + while fg_scaled_x < im_boundry[0] or fg_scaled_y < im_boundry[1]: + fg_scaled_x = fg_scaled_x + float(fg_scaled_x / 100) + fg_scaled_y = fg_scaled_y + float(fg_scaled_y / 100) + + fg_scaled_x = int(fg_scaled_x) + fg_scaled_y = int(fg_scaled_y) + fg_scaled_x_offset = round((fg_scaled_x - im_boundry[0]) / 2) + fg_scaled_y_offset = round((fg_scaled_y - im_boundry[1]) / 2) + + # resize + fg = fg.resize((fg_scaled_x, fg_scaled_y), Image.ANTIALIAS) + + # Paste image + img.paste(fg, (im_topleft[0] - fg_scaled_x_offset, im_topleft[1] - fg_scaled_y_offset), + fg.convert('RGBA')) + img.paste(bg, (0, 0), bg.convert('RGBA')) + + # elif image_overlay_type == 'frame': + else: + fg = Image.open(f"./config/images/{overlay_name}.png") + bg = Image.open(img) + + img = Image.new("RGBA", bg.size) + str_filename = f"./config/images/{overlay_name}.png" + + bg_width, bg_height = bg.size + + fg_scaled_x, fg_scaled_y = fg.size + fg_scaled_x = float(fg_scaled_x / 100) + fg_scaled_y = float(fg_scaled_y / 100) + + max_image_width = 15 + + while fg_scaled_x < ((bg_width / 100) * max_image_width): + fg_scaled_x = fg_scaled_x + float(fg_scaled_x / 100) + fg_scaled_y = fg_scaled_y + float(fg_scaled_y / 100) + + fg_scaled_x = int(fg_scaled_x) + fg_scaled_y = int(fg_scaled_y) + + # resize + fg = fg.resize((fg_scaled_x, fg_scaled_y), Image.ANTIALIAS) + + # Paste image + img.paste(bg, (0, 0), bg.convert('RGBA')) + img.paste(fg, (0, 0), fg.convert('RGBA')) + + # img.save(str_filename) + return img + + @classmethod + async def img_edit_effect(cls, img, effect_type, ctx, cycles: int): + while cycles >= 0: + img = img.filter(effect_type) + cycles = cycles-1 + + return await cls.pil_to_discordfile(img) + + @staticmethod + def get_image_transparency_boundry(image_overlay): + im = Image.open(image_overlay) + image_x, image_y = im.size + iter_x = iter_y = 1 + top_co = None + lef_co = im.size + bot_co = [1, 1] + rig_co = [1, 1] + + while iter_y < image_y: + while iter_x < image_x: + ret_pixel = im.getpixel((iter_x, iter_y)) + + # if str(ret_pixel[3]) == "0": + if int(ret_pixel[3]) < 255: + # top pixel will be the first y coordinate match + if top_co is None: + top_co = [iter_x, iter_y] + + # Check the x iteration is less than lef_co + if iter_x < lef_co[0]: + lef_co = [iter_x, iter_y] + + # Check the x iteration is less than lef_co + if iter_x > rig_co[0]: + rig_co = [iter_x, iter_y] + + # Set bot_co to iter_y + bot_co = [iter_x, iter_y] + + iter_x = iter_x + 1 + iter_y = iter_y + 1 + iter_x = 1 + + top_left = [lef_co[0], top_co[1]] + bottom_right = [rig_co[0], bot_co[1]] + + boundry_size = [bottom_right[0] - top_left[0], bottom_right[1] - top_left[1]] + # print(f"lef_co: {lef_co}, top_co: {top_co}, rig_co: {rig_co}, bot_co: {bot_co}") + # print(f"Co-ordinate 1: ({top_left}), Co-ordinate 2: ({bottom_right}). Boundry size: {boundry_size}") + return boundry_size, top_left + + +def setup(bot): + print("INFO: Loading [Image]... ", end="") + bot.add_cog(ImageCmd(bot)) + print("Done!") + + +def teardown(bot): + print("INFO: Unloading [Image]") + + +class ImageList: + def __init__(self): + self.image_thumbnail_scale = (100, 100) + self.images_in_dir = [] + self.get_images() + self.image_scale = None + self.get_base_image_scale() + + self.x_offset = 0 + self.y_offset = 0 + + self.max_x_size = 96 + self.max_y_size = 80 + + self.image = Image.new('RGBA', self.image_scale, color='#202020') + return + + def get_base_image_scale(self): + row_count = math.ceil(len(self.images_in_dir) / math.ceil(math.sqrt(len(self.images_in_dir)))) + column_count = math.ceil(len(self.images_in_dir) / row_count) + self.image_scale = (row_count*100, column_count*100) + + def get_images(self): + for file in os.listdir(f"./config/images/"): + self.images_in_dir.append(f"{file.split('.')[0]}") + + # Return formatted lists + if len(self.images_in_dir) > 0: + self.images_in_dir.sort() + + def generate_image_list(self): + for image_name in self.images_in_dir: + print(f"Processing {image_name}...") + image_template = Image.open(f'./config/images/{image_name}.png') + image_scale_x, image_scale_y = image_template.size + image_scale_x_scaled = round(image_scale_x / self.image_thumbnail_scale[0]) + image_scale_y_scaled = round(image_scale_y / self.image_thumbnail_scale[1]) + while True: + image_scale_x_scaled += round((image_scale_x / self.image_thumbnail_scale[0])) + image_scale_y_scaled += round((image_scale_y / self.image_thumbnail_scale[1])) + + if image_scale_x_scaled >= self.max_x_size or image_scale_y_scaled >= self.max_y_size: + if image_scale_x_scaled > self.max_x_size: + image_scale_x_scaled = self.max_x_size + if image_scale_y_scaled > self.max_y_size: + image_scale_y_scaled = self.max_y_size + break + + # Determine the centered X of the thumbnails position + centering_image_x = (round(self.image_thumbnail_scale[0] / 2)) - (round(image_scale_x_scaled / 2)) + + # Place the green in the background + image_green = Image.new('RGBA', (image_scale_x_scaled, image_scale_y_scaled), color='#00FF00') + self.image.paste(image_green, (self.x_offset + centering_image_x, self.y_offset)) + + # Place the template in the foreground with it's own mask to see the green + image_template = image_template.resize((image_scale_x_scaled, image_scale_y_scaled)) + + self.image.paste(image_template, (self.x_offset + centering_image_x, self.y_offset), mask=image_template) + + d = ImageDraw.Draw(self.image) + font = ImageFont.truetype("./fonts/roboto/Roboto-Regular.ttf", 10) + font_colour = (255, 255, 255, 255) + name_w, name_h = d.textsize(image_name, font=font) + + d.text((self.x_offset + (100-name_w)/2, self.y_offset+80), image_name, font=font, fill=font_colour) + + self.x_offset += self.image_thumbnail_scale[0] + if self.x_offset >= self.image_scale[0]: + self.x_offset = 0 + self.y_offset += self.image_thumbnail_scale[0] + + async def get_image_for_discord(self, filename='image.png'): + temp_image = BytesIO() + self.image.save(temp_image, 'PNG') + temp_image.seek(0) + return discord.File(fp=temp_image, filename=filename) -- cgit v1.2.3