You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

469 lines
16KB

  1. # -*- coding: utf-8 -*-
  2. # MIT License
  3. #
  4. # Copyright (c) 2017-2018 Roxanne Gibson
  5. #
  6. # Permission is hereby granted, free of charge, to any person obtaining a copy
  7. # of this software and associated documentation files (the "Software"), to deal
  8. # in the Software without restriction, including without limitation the rights
  9. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. # copies of the Software, and to permit persons to whom the Software is
  11. # furnished to do so, subject to the following conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be included in all
  14. # copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22. # SOFTWARE.
  23. import math
  24. import os
  25. import random
  26. import discord
  27. import numpy as np
  28. from PIL import Image, ImageEnhance
  29. from discord.ext import commands
  30. import roxbot
  31. class PrideFlags:
  32. """Class to produce pride flags for the filters in Roxbot."""
  33. def __init__(self, rows=0, colours=None, ratio=None):
  34. self.rows = rows
  35. self.colours = colours
  36. self.ratio = ratio or tuple([(1/rows)]*rows) # Custom ratio is here for things like the bi pride flag
  37. @classmethod
  38. def lgbt(cls):
  39. rows = 6
  40. red = (243, 28, 28)
  41. orange = (255, 196, 0)
  42. yellow = (255, 247, 0)
  43. green = (0, 188, 108)
  44. blue = (0, 149, 255)
  45. violet = (181, 46, 193)
  46. colours = (red, orange, yellow, green, blue, violet)
  47. return cls(rows=rows, colours=colours)
  48. @classmethod
  49. def trans(cls):
  50. rows = 5
  51. blue = (91, 206, 250)
  52. pink = (245, 169, 184)
  53. white = (255, 255, 255)
  54. colours = (blue, pink, white, pink, blue)
  55. return cls(rows=rows, colours=colours)
  56. @classmethod
  57. def non_binary(cls):
  58. rows = 4
  59. yellow = (255, 244, 51)
  60. white = (255, 255, 255)
  61. purple = (155, 89, 208)
  62. grey = (45, 45, 45)
  63. colours = (yellow, white, purple, grey)
  64. return cls(rows=rows, colours=colours)
  65. @classmethod
  66. def bi(cls):
  67. rows = 3
  68. ratio = (0.4, 0.2, 0.4)
  69. pink = (215, 2, 112)
  70. lavender = (115, 79, 150)
  71. blue = (0, 56, 168)
  72. colours = (pink, lavender, blue)
  73. return cls(rows=rows, colours=colours, ratio=ratio)
  74. @classmethod
  75. def pan(cls):
  76. rows = 3
  77. pink = (255, 33, 140)
  78. yellow = (255, 216, 0)
  79. blue = (33, 177, 255)
  80. colours = (pink, yellow, blue)
  81. return cls(rows=rows, colours=colours)
  82. @classmethod
  83. def ace(cls):
  84. rows = 4
  85. black = (0, 0, 0)
  86. grey = (163, 163, 163)
  87. white = (255, 255, 255)
  88. purple = (128, 0, 128)
  89. colours = (black, grey, white, purple)
  90. return cls(rows=rows, colours=colours)
  91. @classmethod
  92. def gq(cls):
  93. rows = 3
  94. purple = (181, 126, 220)
  95. white = (255, 255, 255)
  96. green = (74, 129, 35)
  97. colours = (purple, white, green)
  98. return cls(rows=rows, colours=colours)
  99. @classmethod
  100. def gf(cls):
  101. rows = 5
  102. pink = (255, 117, 162)
  103. white = (255, 255, 255)
  104. purple = (190, 24, 214)
  105. black = (0, 0, 0)
  106. blue = (51, 62, 189)
  107. colours = (pink, white, purple, black, blue)
  108. return cls(rows=rows, colours=colours)
  109. @classmethod
  110. def agender(cls):
  111. rows = 7
  112. black = (0, 0, 0)
  113. white = (255, 255, 255)
  114. grey = (185, 185, 185)
  115. green = (176, 244, 141)
  116. colours = (black, grey, white, green, white, grey, black)
  117. return cls(rows=rows, colours=colours)
  118. class ImageEditor(commands.Cog):
  119. """The ImageEditor cog is a cog with multiple commands to manipulate images provided by the user."""
  120. def __init__(self, bot_client):
  121. self.bot = bot_client
  122. @staticmethod
  123. def image_lookup(message):
  124. try:
  125. if message.attachments[0].height: # Check if attachment is image
  126. return message.attachments[0].url
  127. except IndexError:
  128. return message.author.avatar_url_as(format="png")
  129. @staticmethod
  130. def add_grain(img, prob=0.2, opacity=30):
  131. """
  132. Adds salt and pepper grain to the given image.
  133. :param img: :type PIL.Image: Image to add grain to
  134. :param prob: :type float: Probability of a pixel being black between 0-1
  135. :param opacity: :type int: opacity of the grain when composite with the given image between 0%-100%
  136. :return: :type PIL.Image: Image with added grain
  137. """
  138. img_matrix = np.zeros((img.height, img.width), dtype=np.uint8)
  139. for y in range(img.height):
  140. for x in range(img.width):
  141. if prob < random.random():
  142. img_matrix[y][x] = 255
  143. noisy = Image.fromarray(img_matrix, "L")
  144. noisy = noisy.convert("RGB")
  145. mask = Image.new('RGBA', img.size, (0, 0, 0, opacity))
  146. return Image.composite(noisy, img, mask)
  147. @staticmethod
  148. async def flag_filter(name, flag, url):
  149. """At the moment, can only make horizontal stripe flags"""
  150. f = 'filter_{}.png'.format(name)
  151. await roxbot.http.download_file(url, f)
  152. ava = Image.open(f)
  153. top = 0 # In the box we use, top is used to define which part of the image we are working on
  154. bottom = 0 # And bottom defines the height. That should help you visualise why I increment the values the way I do
  155. for x, colour in enumerate(flag.colours):
  156. # Grab the next slice of the images height and width
  157. # we use math.ceil here to avoid rounding errors when converting float to int
  158. height = int(math.ceil(ava.height * flag.ratio[x]))
  159. width = ava.width
  160. bottom += height
  161. box = (0, top, width, bottom)
  162. # Make the colour block and the transparency mask at the slice size. Then crop the next part of the image
  163. row = Image.new('RGB', (width, height), colour)
  164. mask = Image.new('RGBA', (width, height), (0, 0, 0, 123))
  165. crop = ava.crop(box)
  166. # Combine all three and paste it back into original image
  167. part = Image.composite(crop, row, mask)
  168. ava.paste(part, box)
  169. top += height
  170. os.remove(f)
  171. ava.save(f)
  172. file = discord.File(f)
  173. return file
  174. async def image_logging(self, ctx, output):
  175. """Logging function for all image commands to avoid shit loads or repeating code.
  176. Required because image has outputs that are user decided and therefore could need logging for."""
  177. return await self.bot.log(
  178. ctx.guild,
  179. "image",
  180. User=ctx.author,
  181. User_ID=ctx.author.id,
  182. Output_Message_ID=output.id,
  183. Channel=ctx.channel,
  184. Channel_Mention=ctx.channel.mention,
  185. Time="{:%a %Y/%m/%d %H:%M:%S} UTC".format(ctx.message.created_at)
  186. )
  187. @commands.group(case_insensitive=True)
  188. async def pride(self, ctx):
  189. """`;pride` is a command group for multiple pride flag filters."""
  190. if ctx.invoked_subcommand is None:
  191. raise commands.CommandNotFound("Subcommand '{}' does not exist.".format(ctx.subcommand_passed))
  192. @pride.command()
  193. async def lgbt(self, ctx, image: roxbot.converters.AvatarURL=None):
  194. """Adds a LGBT Pride Flag filter to the given image
  195. Args:
  196. image: Optional
  197. If nothing, your avatar
  198. Mention a user, their avatar
  199. Provide a URL, that image
  200. Provide an image via upload, that image.
  201. """
  202. if not image:
  203. image = self.image_lookup(ctx.message)
  204. flag = PrideFlags.lgbt()
  205. async with ctx.typing():
  206. file = await self.flag_filter("lgbt", flag, image)
  207. output = await ctx.send(file=file)
  208. os.remove(file.filename)
  209. await self.image_logging(ctx, output)
  210. @pride.command(aliases=["trans"])
  211. async def transgender(self, ctx, image: roxbot.converters.AvatarURL=None):
  212. """Adds a Trans Pride Flag filter to the given image
  213. Args:
  214. image: Optional
  215. If nothing, your avatar
  216. Mention a user, their avatar
  217. Provide a URL, that image
  218. Provide an image via upload, that image.
  219. """
  220. if not image:
  221. image = self.image_lookup(ctx.message)
  222. flag = PrideFlags.trans()
  223. async with ctx.typing():
  224. file = await self.flag_filter("trans", flag, image)
  225. output = await ctx.send(file=file)
  226. os.remove(file.filename)
  227. await self.image_logging(ctx, output)
  228. @pride.command(aliases=["nb", "enby"])
  229. async def nonbinary(self, ctx, image: roxbot.converters.AvatarURL=None):
  230. """Adds a Non-Binary Pride Flag filter to the given image
  231. Args:
  232. image: Optional
  233. If nothing, your avatar
  234. Mention a user, their avatar
  235. Provide a URL, that image
  236. Provide an image via upload, that image.
  237. """
  238. if not image:
  239. image = self.image_lookup(ctx.message)
  240. flag = PrideFlags.non_binary()
  241. async with ctx.typing():
  242. file = await self.flag_filter("nb", flag, image)
  243. output = await ctx.send(file=file)
  244. os.remove(file.filename)
  245. await self.image_logging(ctx, output)
  246. @pride.command(aliases=["bi"])
  247. async def bisexual(self, ctx, image: roxbot.converters.AvatarURL=None):
  248. """Adds a Bisexual Pride Flag filter to the given image
  249. Args:
  250. image: Optional
  251. If nothing, your avatar
  252. Mention a user, their avatar
  253. Provide a URL, that image
  254. Provide an image via upload, that image.
  255. """
  256. if not image:
  257. image = self.image_lookup(ctx.message)
  258. flag = PrideFlags.bi()
  259. async with ctx.typing():
  260. file = await self.flag_filter("bi", flag, image)
  261. output = await ctx.send(file=file)
  262. os.remove(file.filename)
  263. await self.image_logging(ctx, output)
  264. @pride.command(aliases=["gq"])
  265. async def genderqueer(self, ctx, image: roxbot.converters.AvatarURL=None):
  266. """Adds a Gender Queer Pride Flag filter to the given image
  267. Args:
  268. image: Optional
  269. If nothing, your avatar
  270. Mention a user, their avatar
  271. Provide a URL, that image
  272. Provide an image via upload, that image.
  273. """
  274. if not image:
  275. image = self.image_lookup(ctx.message)
  276. flag = PrideFlags.gq()
  277. async with ctx.typing():
  278. file = await self.flag_filter("gq", flag, image)
  279. output = await ctx.send(file=file)
  280. os.remove(file.filename)
  281. await self.image_logging(ctx, output)
  282. @pride.command(aliases=["pan"])
  283. async def pansexual(self, ctx, image: roxbot.converters.AvatarURL=None):
  284. """Adds a Pansexual Pride Flag filter to the given image
  285. Args:
  286. image: Optional
  287. If nothing, your avatar
  288. Mention a user, their avatar
  289. Provide a URL, that image
  290. Provide an image via upload, that image.
  291. """
  292. if not image:
  293. image = self.image_lookup(ctx.message)
  294. flag = PrideFlags.pan()
  295. async with ctx.typing():
  296. file = await self.flag_filter("pan", flag, image)
  297. output = await ctx.send(file=file)
  298. os.remove(file.filename)
  299. await self.image_logging(ctx, output)
  300. @pride.command(aliases=["ace"])
  301. async def asexual(self, ctx, image: roxbot.converters.AvatarURL=None):
  302. """Adds an Asexual Pride Flag filter to the given image
  303. Args:
  304. image: Optional
  305. If nothing, your avatar
  306. Mention a user, their avatar
  307. Provide a URL, that image
  308. Provide an image via upload, that image.
  309. """
  310. if not image:
  311. image = self.image_lookup(ctx.message)
  312. flag = PrideFlags.ace()
  313. async with ctx.typing():
  314. file = await self.flag_filter("ace", flag, image)
  315. output = await ctx.send(file=file)
  316. os.remove(file.filename)
  317. await self.image_logging(ctx, output)
  318. @pride.command(aliases=["gf"])
  319. async def genderfluid(self, ctx, image: roxbot.converters.AvatarURL = None):
  320. """Adds a Gender Fluid Pride Flag filter to the given image
  321. Args:
  322. image: Optional
  323. If nothing, your avatar
  324. Mention a user, their avatar
  325. Provide a URL, that image
  326. Provide an image via upload, that image.
  327. """
  328. if not image:
  329. image = self.image_lookup(ctx.message)
  330. flag = PrideFlags.gf()
  331. async with ctx.typing():
  332. file = await self.flag_filter("gf", flag, image)
  333. output = await ctx.send(file=file)
  334. os.remove(file.filename)
  335. await self.image_logging(ctx, output)
  336. @pride.command()
  337. async def agender(self, ctx, image: roxbot.converters.AvatarURL = None):
  338. """Adds an Agender Pride Flag filter to the given image
  339. Args:
  340. image: Optional
  341. If nothing, your avatar
  342. Mention a user, their avatar
  343. Provide a URL, that image
  344. Provide an image via upload, that image.
  345. """
  346. if not image:
  347. image = self.image_lookup(ctx.message)
  348. flag = PrideFlags.agender()
  349. async with ctx.typing():
  350. file = await self.flag_filter("agender", flag, image)
  351. output = await ctx.send(file=file)
  352. os.remove(file.filename)
  353. await self.image_logging(ctx, output)
  354. @commands.command(aliases=["df"])
  355. async def deepfry(self, ctx, image: roxbot.converters.AvatarURL=None):
  356. """Deepfrys the given image
  357. Args:
  358. image: Optional
  359. If nothing, your avatar
  360. Mention a user, their avatar
  361. Provide a URL, that image
  362. Provide an image via upload, that image.
  363. """
  364. if not image:
  365. image = self.image_lookup(ctx.message)
  366. filename = await roxbot.http.download_file(image)
  367. async with ctx.typing():
  368. # Convert to jpg
  369. if filename.split(".")[-1] != "jpg":
  370. jpg_name = filename.split(".")[0] + ".jpg"
  371. img = Image.open(filename)
  372. img = img.convert(mode="RGB")
  373. img.save(jpg_name)
  374. os.remove(filename)
  375. else:
  376. jpg_name = filename
  377. img = Image.open(jpg_name)
  378. # Brightness Enhance
  379. ehn = ImageEnhance.Brightness(img)
  380. img = ehn.enhance(1.25)
  381. # Contrast Enhance
  382. ehn = ImageEnhance.Contrast(img)
  383. img = ehn.enhance(1.5)
  384. # Sharpness Enhance
  385. ehn = ImageEnhance.Sharpness(img)
  386. img = ehn.enhance(20)
  387. # Saturation Enhance
  388. ehn = ImageEnhance.Color(img)
  389. img = ehn.enhance(2)
  390. # Add Salt and Pepper Noise
  391. img = self.add_grain(img)
  392. img.save(jpg_name)
  393. # JPG-fy image
  394. for x in range(20):
  395. img = Image.open(jpg_name)
  396. img = img.convert(mode="RGB")
  397. img.save(jpg_name)
  398. output = await ctx.send(file=discord.File(jpg_name))
  399. os.remove(jpg_name)
  400. await self.image_logging(ctx, output)
  401. def setup(bot_client):
  402. bot_client.add_cog(ImageEditor(bot_client))