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.

598 lines
20KB

  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 Flag:
  32. """Class to produce pride flags for the filters in Roxbot."""
  33. def __init__(self, colours=None, ratio=None, name=""):
  34. self.name = name
  35. self.rows = len(colours)
  36. self.colours = colours
  37. self.ratio = ratio or tuple([(1/self.rows)]*self.rows) # Custom ratio is here for things like the bi pride flag
  38. @classmethod
  39. def lgbt(cls):
  40. name = "lgbt"
  41. red = (243, 28, 28)
  42. orange = (255, 196, 0)
  43. yellow = (255, 247, 0)
  44. green = (0, 188, 108)
  45. blue = (0, 149, 255)
  46. violet = (181, 46, 193)
  47. colours = (red, orange, yellow, green, blue, violet)
  48. return cls(colours=colours, name=name)
  49. @classmethod
  50. def trans(cls):
  51. name = "trans"
  52. blue = (91, 206, 250)
  53. pink = (245, 169, 184)
  54. white = (255, 255, 255)
  55. colours = (blue, pink, white, pink, blue)
  56. return cls(colours=colours, name=name)
  57. @classmethod
  58. def non_binary(cls):
  59. name = "nb"
  60. yellow = (255, 244, 51)
  61. white = (255, 255, 255)
  62. purple = (155, 89, 208)
  63. grey = (45, 45, 45)
  64. colours = (yellow, white, purple, grey)
  65. return cls(colours=colours, name=name)
  66. @classmethod
  67. def bi(cls):
  68. name = "bi"
  69. ratio = (0.4, 0.2, 0.4)
  70. pink = (215, 2, 112)
  71. lavender = (115, 79, 150)
  72. blue = (0, 56, 168)
  73. colours = (pink, lavender, blue)
  74. return cls(colours=colours, ratio=ratio, name=name)
  75. @classmethod
  76. def pan(cls):
  77. name = "pan"
  78. pink = (255, 33, 140)
  79. yellow = (255, 216, 0)
  80. blue = (33, 177, 255)
  81. colours = (pink, yellow, blue)
  82. return cls(colours=colours, name=name)
  83. @classmethod
  84. def ace(cls):
  85. name = "ace"
  86. black = (0, 0, 0)
  87. grey = (163, 163, 163)
  88. white = (255, 255, 255)
  89. purple = (128, 0, 128)
  90. colours = (black, grey, white, purple)
  91. return cls(colours=colours, name=name)
  92. @classmethod
  93. def gq(cls):
  94. name = "gq"
  95. purple = (181, 126, 220)
  96. white = (255, 255, 255)
  97. green = (74, 129, 35)
  98. colours = (purple, white, green)
  99. return cls(colours=colours, name=name)
  100. @classmethod
  101. def gf(cls):
  102. name = "genderflu"
  103. pink = (255, 117, 162)
  104. white = (255, 255, 255)
  105. purple = (190, 24, 214)
  106. black = (0, 0, 0)
  107. blue = (51, 62, 189)
  108. colours = (pink, white, purple, black, blue)
  109. return cls(colours=colours, name=name)
  110. @classmethod
  111. def agender(cls):
  112. name = "agender"
  113. black = (0, 0, 0)
  114. white = (255, 255, 255)
  115. grey = (185, 185, 185)
  116. green = (176, 244, 141)
  117. colours = (black, grey, white, green, white, grey, black)
  118. return cls(colours=colours, name=name)
  119. @classmethod
  120. def aro(cls):
  121. name = "aro"
  122. green = (61, 165, 66)
  123. ltgreen = (167, 212, 121)
  124. white = (255, 255, 255)
  125. grey = (169, 169, 169)
  126. black = (0, 0, 0)
  127. colours = (green, ltgreen, white, grey, black)
  128. return cls(colours=colours, name=name)
  129. @classmethod
  130. def demigirl(cls):
  131. name = "demigirl"
  132. grey = (128, 128, 128)
  133. silver = (196, 196, 196)
  134. pink = (254, 176, 202)
  135. white = (255, 255, 255)
  136. colours = (grey, silver, pink, white, pink, silver, grey)
  137. return cls(colours=colours, name=name)
  138. @classmethod
  139. def demiboy(cls):
  140. name = "demiboy"
  141. grey = (128, 128, 128)
  142. silver = (196, 196, 196)
  143. blue = (155, 218, 235)
  144. white = (255, 255, 255)
  145. colours = (grey, silver, blue, white, blue, silver, grey)
  146. return cls(colours=colours, name=name)
  147. @classmethod
  148. def deminb(cls):
  149. name = "deminb"
  150. grey = (128, 128, 128)
  151. silver = (196, 196, 196)
  152. yellow = (251, 255, 117)
  153. white = (255, 255, 255)
  154. colours = (grey, silver, yellow, white, yellow, silver, grey)
  155. return cls(colours=colours, name=name)
  156. @classmethod
  157. def polygender(cls):
  158. name = "polygender"
  159. black = (0, 0, 0)
  160. grey = (147, 147, 147)
  161. pink = (237, 148, 197)
  162. yellow = (245, 237, 129)
  163. blue = (100, 187, 230)
  164. colours = (black, grey, pink, yellow, blue)
  165. return cls(colours=colours, name=name)
  166. @classmethod
  167. def polysexual(cls):
  168. name = "polysexual"
  169. pink = (246, 22, 186)
  170. green = (0, 214, 105)
  171. blue = (21, 147, 246)
  172. colours = (pink, green, blue)
  173. return cls(colours=colours, name=name)
  174. class ImageEditor(commands.Cog):
  175. """The ImageEditor cog is a cog with multiple commands to manipulate images provided by the user."""
  176. def __init__(self, bot_client):
  177. self.bot = bot_client
  178. @staticmethod
  179. def image_lookup(message):
  180. try:
  181. if message.attachments[0].height: # Check if attachment is image
  182. return message.attachments[0].url
  183. except IndexError:
  184. return message.author.avatar_url_as(format="png")
  185. @staticmethod
  186. def add_grain(img, prob=0.2, opacity=30):
  187. """
  188. Adds salt and pepper grain to the given image.
  189. :param img: :type PIL.Image: Image to add grain to
  190. :param prob: :type float: Probability of a pixel being black between 0-1
  191. :param opacity: :type int: opacity of the grain when composite with the given image between 0%-100%
  192. :return: :type PIL.Image: Image with added grain
  193. """
  194. img_matrix = np.zeros((img.height, img.width), dtype=np.uint8)
  195. for y in range(img.height):
  196. for x in range(img.width):
  197. if prob < random.random():
  198. img_matrix[y][x] = 255
  199. noisy = Image.fromarray(img_matrix, "L")
  200. noisy = noisy.convert("RGB")
  201. mask = Image.new('RGBA', img.size, (0, 0, 0, opacity))
  202. return Image.composite(noisy, img, mask)
  203. @staticmethod
  204. async def flag_filter(flag, url):
  205. """At the moment, can only make horizontal stripe flags"""
  206. f = 'filter_{}.png'.format(flag.name)
  207. await roxbot.http.download_file(url, f)
  208. ava = Image.open(f)
  209. top = 0 # In the box we use, top is used to define which part of the image we are working on
  210. bottom = 0 # And bottom defines the height. That should help you visualise why I increment the values the way I do
  211. for x, colour in enumerate(flag.colours):
  212. # Grab the next slice of the images height and width
  213. # we use math.ceil here to avoid rounding errors when converting float to int
  214. height = int(math.ceil(ava.height * flag.ratio[x]))
  215. width = ava.width
  216. bottom += height
  217. box = (0, top, width, bottom)
  218. # Make the colour block and the transparency mask at the slice size. Then crop the next part of the image
  219. row = Image.new('RGB', (width, height), colour)
  220. mask = Image.new('RGBA', (width, height), (0, 0, 0, 123))
  221. crop = ava.crop(box)
  222. # Combine all three and paste it back into original image
  223. part = Image.composite(crop, row, mask)
  224. ava.paste(part, box)
  225. top += height
  226. os.remove(f)
  227. ava.save(f)
  228. file = discord.File(f)
  229. return file
  230. async def image_logging(self, ctx, output):
  231. """Logging function for all image commands to avoid shit loads or repeating code.
  232. Required because image has outputs that are user decided and therefore could need logging for."""
  233. if isinstance(ctx.channel, discord.TextChannel):
  234. return await self.bot.log(
  235. ctx.guild,
  236. "image",
  237. User=ctx.author,
  238. User_ID=ctx.author.id,
  239. Output_Message_ID=output.id,
  240. Channel=ctx.channel,
  241. Channel_Mention=ctx.channel.mention,
  242. Time="{:%a %Y/%m/%d %H:%M:%S} UTC".format(ctx.message.created_at)
  243. )
  244. @commands.group(case_insensitive=True)
  245. async def pride(self, ctx):
  246. """`;pride` is a command group for multiple pride flag filters."""
  247. if ctx.invoked_subcommand is None:
  248. raise commands.CommandNotFound("Subcommand '{}' does not exist.".format(ctx.subcommand_passed))
  249. async def pride_flag_posting(self, ctx, flag, image):
  250. async with ctx.typing():
  251. file = await self.flag_filter(flag, image)
  252. output = await ctx.send(file=file)
  253. os.remove(file.filename)
  254. await self.image_logging(ctx, output)
  255. @pride.command()
  256. async def lgbt(self, ctx, image: roxbot.converters.AvatarURL=None):
  257. """Adds a LGBT Pride Flag filter to the given image
  258. Args:
  259. image: Optional
  260. If nothing, your avatar
  261. Mention a user, their avatar
  262. Provide a URL, that image
  263. Provide an image via upload, that image.
  264. """
  265. if not image:
  266. image = self.image_lookup(ctx.message)
  267. flag = Flag.lgbt()
  268. await self.pride_flag_posting(ctx, flag, image)
  269. @pride.command(aliases=["trans"])
  270. async def transgender(self, ctx, image: roxbot.converters.AvatarURL=None):
  271. """Adds a Trans Pride Flag filter to the given image
  272. Args:
  273. image: Optional
  274. If nothing, your avatar
  275. Mention a user, their avatar
  276. Provide a URL, that image
  277. Provide an image via upload, that image.
  278. """
  279. if not image:
  280. image = self.image_lookup(ctx.message)
  281. flag = Flag.trans()
  282. await self.pride_flag_posting(ctx, flag, image)
  283. @pride.command(aliases=["nb", "enby"])
  284. async def nonbinary(self, ctx, image: roxbot.converters.AvatarURL=None):
  285. """Adds a Non-Binary Pride Flag filter to the given image
  286. Args:
  287. image: Optional
  288. If nothing, your avatar
  289. Mention a user, their avatar
  290. Provide a URL, that image
  291. Provide an image via upload, that image.
  292. """
  293. if not image:
  294. image = self.image_lookup(ctx.message)
  295. flag = Flag.non_binary()
  296. await self.pride_flag_posting(ctx, flag, image)
  297. @pride.command(aliases=["bi"])
  298. async def bisexual(self, ctx, image: roxbot.converters.AvatarURL=None):
  299. """Adds a Bisexual Pride Flag filter to the given image
  300. Args:
  301. image: Optional
  302. If nothing, your avatar
  303. Mention a user, their avatar
  304. Provide a URL, that image
  305. Provide an image via upload, that image.
  306. """
  307. if not image:
  308. image = self.image_lookup(ctx.message)
  309. flag = Flag.bi()
  310. await self.pride_flag_posting(ctx, flag, image)
  311. @pride.command(aliases=["gq"])
  312. async def genderqueer(self, ctx, image: roxbot.converters.AvatarURL=None):
  313. """Adds a Gender Queer Pride Flag filter to the given image
  314. Args:
  315. image: Optional
  316. If nothing, your avatar
  317. Mention a user, their avatar
  318. Provide a URL, that image
  319. Provide an image via upload, that image.
  320. """
  321. if not image:
  322. image = self.image_lookup(ctx.message)
  323. flag = Flag.gq()
  324. await self.pride_flag_posting(ctx, flag, image)
  325. @pride.command(aliases=["pan"])
  326. async def pansexual(self, ctx, image: roxbot.converters.AvatarURL=None):
  327. """Adds a Pansexual Pride Flag filter to the given image
  328. Args:
  329. image: Optional
  330. If nothing, your avatar
  331. Mention a user, their avatar
  332. Provide a URL, that image
  333. Provide an image via upload, that image.
  334. """
  335. if not image:
  336. image = self.image_lookup(ctx.message)
  337. flag = Flag.pan()
  338. await self.pride_flag_posting(ctx, flag, image)
  339. @pride.command(aliases=["ace"])
  340. async def asexual(self, ctx, image: roxbot.converters.AvatarURL=None):
  341. """Adds an Asexual Pride Flag filter to the given image
  342. Args:
  343. image: Optional
  344. If nothing, your avatar
  345. Mention a user, their avatar
  346. Provide a URL, that image
  347. Provide an image via upload, that image.
  348. """
  349. if not image:
  350. image = self.image_lookup(ctx.message)
  351. flag = Flag.ace()
  352. await self.pride_flag_posting(ctx, flag, image)
  353. @pride.command(aliases=["gf"])
  354. async def genderfluid(self, ctx, image: roxbot.converters.AvatarURL = None):
  355. """Adds a Gender Fluid Pride Flag filter to the given image
  356. Args:
  357. image: Optional
  358. If nothing, your avatar
  359. Mention a user, their avatar
  360. Provide a URL, that image
  361. Provide an image via upload, that image.
  362. """
  363. if not image:
  364. image = self.image_lookup(ctx.message)
  365. flag = Flag.gf()
  366. await self.pride_flag_posting(ctx, flag, image)
  367. @pride.command()
  368. async def agender(self, ctx, image: roxbot.converters.AvatarURL = None):
  369. """Adds an Agender Pride Flag filter to the given image
  370. Args:
  371. image: Optional
  372. If nothing, your avatar
  373. Mention a user, their avatar
  374. Provide a URL, that image
  375. Provide an image via upload, that image.
  376. """
  377. if not image:
  378. image = self.image_lookup(ctx.message)
  379. flag = Flag.agender()
  380. await self.pride_flag_posting(ctx, flag, image)
  381. @pride.command(aliases=["aro"])
  382. async def aromantic(self, ctx, image: roxbot.converters.AvatarURL = None):
  383. """Adds a Aromantic Pride Flag filter to the given image
  384. Args:
  385. image: Optional
  386. If nothing, your avatar
  387. Mention a user, their avatar
  388. Provide a URL, that image
  389. Provide an image via upload, that image.
  390. """
  391. if not image:
  392. image = self.image_lookup(ctx.message)
  393. flag = Flag.aro()
  394. await self.pride_flag_posting(ctx, flag, image)
  395. @pride.command(aliases=[])
  396. async def demigirl(self, ctx, image: roxbot.converters.AvatarURL = None):
  397. """Adds a Demi Girl Pride Flag filter to the given image
  398. Args:
  399. image: Optional
  400. If nothing, your avatar
  401. Mention a user, their avatar
  402. Provide a URL, that image
  403. Provide an image via upload, that image.
  404. """
  405. if not image:
  406. image = self.image_lookup(ctx.message)
  407. flag = Flag.demigirl()
  408. await self.pride_flag_posting(ctx, flag, image)
  409. @pride.command(aliases=[])
  410. async def demiboy(self, ctx, image: roxbot.converters.AvatarURL = None):
  411. """Adds a Demi Boy Pride Flag filter to the given image
  412. Args:
  413. image: Optional
  414. If nothing, your avatar
  415. Mention a user, their avatar
  416. Provide a URL, that image
  417. Provide an image via upload, that image.
  418. """
  419. if not image:
  420. image = self.image_lookup(ctx.message)
  421. flag = Flag.demiboy()
  422. await self.pride_flag_posting(ctx, flag, image)
  423. @pride.command(aliases=["deminonbinary"])
  424. async def deminb(self, ctx, image: roxbot.converters.AvatarURL = None):
  425. """Adds a Demi non-binary Pride Flag filter to the given image
  426. Args:
  427. image: Optional
  428. If nothing, your avatar
  429. Mention a user, their avatar
  430. Provide a URL, that image
  431. Provide an image via upload, that image.
  432. """
  433. if not image:
  434. image = self.image_lookup(ctx.message)
  435. flag = Flag.deminb()
  436. await self.pride_flag_posting(ctx, flag, image)
  437. @pride.command(aliases=[])
  438. async def polygender(self, ctx, image: roxbot.converters.AvatarURL = None):
  439. """Adds a Polygender Pride Flag filter to the given image
  440. Args:
  441. image: Optional
  442. If nothing, your avatar
  443. Mention a user, their avatar
  444. Provide a URL, that image
  445. Provide an image via upload, that image.
  446. """
  447. if not image:
  448. image = self.image_lookup(ctx.message)
  449. flag = Flag.polygender()
  450. await self.pride_flag_posting(ctx, flag, image)
  451. @pride.command(aliases=[])
  452. async def polysexual(self, ctx, image: roxbot.converters.AvatarURL = None):
  453. """Adds a Polysexual Pride Flag filter to the given image
  454. Args:
  455. image: Optional
  456. If nothing, your avatar
  457. Mention a user, their avatar
  458. Provide a URL, that image
  459. Provide an image via upload, that image.
  460. """
  461. if not image:
  462. image = self.image_lookup(ctx.message)
  463. flag = Flag.polysexual()
  464. await self.pride_flag_posting(ctx, flag, image)
  465. @commands.command(aliases=["df"])
  466. async def deepfry(self, ctx, image: roxbot.converters.AvatarURL=None):
  467. """Deepfrys the given image
  468. Args:
  469. image: Optional
  470. If nothing, your avatar
  471. Mention a user, their avatar
  472. Provide a URL, that image
  473. Provide an image via upload, that image.
  474. """
  475. if not image:
  476. image = self.image_lookup(ctx.message)
  477. filename = await roxbot.http.download_file(image)
  478. async with ctx.typing():
  479. # Convert to jpg
  480. if filename.split(".")[-1] != "jpg":
  481. jpg_name = filename.split(".")[0] + ".jpg"
  482. img = Image.open(filename)
  483. img = img.convert(mode="RGB")
  484. img.save(jpg_name)
  485. os.remove(filename)
  486. else:
  487. jpg_name = filename
  488. img = Image.open(jpg_name)
  489. # Brightness Enhance
  490. ehn = ImageEnhance.Brightness(img)
  491. img = ehn.enhance(1.25)
  492. # Contrast Enhance
  493. ehn = ImageEnhance.Contrast(img)
  494. img = ehn.enhance(1.5)
  495. # Sharpness Enhance
  496. ehn = ImageEnhance.Sharpness(img)
  497. img = ehn.enhance(20)
  498. # Saturation Enhance
  499. ehn = ImageEnhance.Color(img)
  500. img = ehn.enhance(2)
  501. # Add Salt and Pepper Noise
  502. img = self.add_grain(img)
  503. img.save(jpg_name)
  504. # JPG-fy image
  505. for x in range(20):
  506. img = Image.open(jpg_name)
  507. img = img.convert(mode="RGB")
  508. img.save(jpg_name)
  509. output = await ctx.send(file=discord.File(jpg_name))
  510. os.remove(jpg_name)
  511. await self.image_logging(ctx, output)
  512. def setup(bot_client):
  513. bot_client.add_cog(ImageEditor(bot_client))