diff options
-rw-r--r-- | bot/exts/utilities/color.py | 395 |
1 files changed, 66 insertions, 329 deletions
diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index c1523281..dc63cf84 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,7 +1,7 @@ import colorsys import json import logging -import re +import random from io import BytesIO from discord import Embed, File @@ -10,81 +10,97 @@ from PIL import Image, ImageColor from rapidfuzz import process from bot.bot import Bot -from bot.constants import Colours - -# from bot.exts.core.extension import invoke_help_command - +from bot.exts.core.extensions import invoke_help_command logger = logging.getLogger(__name__) -ERROR_MSG = """The color code {user_color} is not a possible color combination. -The range of possible values are: -RGB & HSV: 0-255 -CMYK: 0-100% -HSL: 0-360 degrees -Hex: #000000-#FFFFFF -""" - with open("bot/resources/utilities/ryanzec_colours.json") as f: COLOR_MAPPING = json.load(f) THUMBNAIL_SIZE = 80 -""" + class Colour(commands.Cog): + """Cog for the Colour command.""" def __init__(self, bot: Bot) -> None: self.bot = bot @commands.group(aliases=["color"]) async def colour(self, ctx: commands.Context) -> None: + """ + User initiated command to create an embed that displays color information. + + For the commands `hsl`, `hsv` and `rgb`: input is in the form `.color <mode> <int> <int> <int>` + For the command `cmyk`: input is in the form `.color cmyk <int> <int> <int> <int>` + For the command `hex`: input is in the form `.color hex #<hex code>` + For the command `name`: input is in the form `.color name <color name>` + For the command `random`: input is in the form `.color random` + """ if ctx.invoked_subcommand is None: await invoke_help_command(ctx) @colour.command() async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: + """Function to create an embed from an RGB input.""" rgb_tuple = ImageColor.getrgb(f"rgb({red}, {green}, {blue})") await Colour.send_colour_response(ctx, list(rgb_tuple)) @colour.command() async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: + """Function to create an embed from an HSV input.""" hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") await Colour.send_colour_response(ctx, list(hsv_tuple)) @colour.command() async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: + """Function to create an embed from an HSL input.""" hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)") await Colour.send_colour_response(ctx, list(hsl_tuple)) @colour.command() async def cmyk(self, ctx: commands.Context, cyan: int, yellow: int, magenta: int, key: int) -> None: - ... + """Function to create an embed from a CMYK input.""" + r = int(255 * (1.0 - cyan / float(100)) * (1.0 - key / float(100))) + g = int(255 * (1.0 - magenta / float(100)) * (1.0 - key / float(100))) + b = int(255 * (1.0 - yellow / float(100)) * (1.0 - key / float(100))) + await Colour.send_colour_response(ctx, list((r, g, b))) @colour.command() async def hex(self, ctx: commands.Context, hex_code: str) -> None: + """Function to create an embed from a HEX input. (Requires # as a prefix).""" hex_tuple = ImageColor.getrgb(hex_code) await Colour.send_colour_response(ctx, list(hex_tuple)) @colour.command() - async def yiq( - self, - ctx: commands.Context, - perceived_luminesence: int, - in_phase: int, - quadrature: int - ) -> None: - yiq_list = list(colorsys.yiq_to_rgb(perceived_luminesence, in_phase, quadrature)) - yiq_tuple = [int(val * 255.0) for val in yiq_list] - await Colour.send_colour_response(ctx, list(yiq_tuple)) + async def name(self, ctx: commands.Context, user_color: str) -> None: + """Function to create an embed from a name input.""" + _, hex_color = self.match_color_name(user_color) + hex_tuple = ImageColor.getrgb(hex_color) + await Colour.send_colour_response(ctx, list(hex_tuple)) + + @colour.command() + async def random(self, ctx: commands.Context) -> None: + """Function to create an embed from a randomly chosen color from the ryanzec.json file.""" + color_choices = list(COLOR_MAPPING.values()) + hex_color = random.choice(color_choices) + hex_tuple = ImageColor.getrgb(f"#{hex_color}") + await Colour.send_colour_response(ctx, list(hex_tuple)) @staticmethod - async def send_colour_response(ctx: commands.Context, rgb: list[int]) -> Message: + async def send_colour_response(ctx: commands.Context, rgb: list[int]) -> None: + """Function to create and send embed from color information.""" r, g, b = rgb[0], rgb[1], rgb[2] + name = Colour._rgb_to_name(rgb) + if name is None: + desc = "Color information for the input color." + else: + desc = f"Color information for {name}" colour_embed = Embed( title="Colour", - description="Here lies thy colour", + description=desc, colour=int(f"{r:02x}{g:02x}{b:02x}", 16) ) colour_conversions = Colour.get_colour_conversions(rgb) @@ -107,17 +123,19 @@ class Colour(commands.Cog): @staticmethod def get_colour_conversions(rgb: list[int]) -> dict[str, str]: + """Create a dictionary mapping of color types and their values.""" return { "rgb": tuple(rgb), "hsv": Colour._rgb_to_hsv(rgb), "hsl": Colour._rgb_to_hsl(rgb), "cmyk": Colour._rgb_to_cmyk(rgb), "hex": Colour._rgb_to_hex(rgb), - "yiq": Colour._rgb_to_yiq(rgb) + "name": Colour._rgb_to_name(rgb) } @staticmethod def _rgb_to_hsv(rgb: list[int]) -> tuple[int, int, int]: + """Function to convert an RGB list to a HSV list.""" rgb = [val / 255.0 for val in rgb] h, v, s = colorsys.rgb_to_hsv(*rgb) hsv = (round(h * 360), round(s * 100), round(v * 100)) @@ -125,6 +143,7 @@ class Colour(commands.Cog): @staticmethod def _rgb_to_hsl(rgb: list[int]) -> tuple[int, int, int]: + """Function to convert an RGB list to a HSL list.""" rgb = [val / 255.0 for val in rgb] h, l, s = colorsys.rgb_to_hls(*rgb) hsl = (round(h * 360), round(s * 100), round(l * 100)) @@ -132,269 +151,39 @@ class Colour(commands.Cog): @staticmethod def _rgb_to_cmyk(rgb: list[int]) -> tuple[int, int, int, int]: + """Function to convert an RGB list to a CMYK list.""" rgb = [val / 255.0 for val in rgb] - if all(val == 0 for val in rgb): return 0, 0, 0, 100 - cmy = [1 - val / 255 for val in rgb] min_cmy = min(cmy) - cmyk = [(val - min_cmy) / (1 - min_cmy) for val in cmy] + [min_cmy] cmyk = [round(val * 100) for val in cmyk] - return tuple(cmyk) @staticmethod def _rgb_to_hex(rgb: list[int]) -> str: + """Function to convert an RGB list to a HEX string.""" hex_ = ''.join([hex(val)[2:].zfill(2) for val in rgb]) hex_code = f"#{hex_}".upper() return hex_code @staticmethod - def _rgb_to_yiq(rgb: list[int]) -> tuple[int, int, int, int]: - rgb = [val / 255.0 for val in rgb] - y, i, q = colorsys.rgb_to_yiq(*rgb) - yiq = (round(y), round(i), round(q)) - return yiq - -def setup(bot: commands.Bot) -> None: - bot.add_cog(Colour(bot)) -""" - - -class Color(commands.Cog): - """User initiated commands to receive color information.""" - - def __init__(self, bot: Bot): - self.bot = bot - - @commands.command(aliases=["colour"]) - async def color(self, ctx: commands.Context, mode: str, *, user_color: str) -> None: - """ - Send information on input color code or color name. - - Possible modes are: "hex", "rgb", "hsv", "hsl", "cmyk" or "name". - """ - logger.debug(f"{mode = }") - logger.debug(f"{user_color = }") - if mode.lower() == "hex": - await self.hex_to_rgb(ctx, user_color) - elif mode.lower() == "rgb": - rgb_color = self.tuple_create(user_color) - await self.color_embed(ctx, rgb_color) - elif mode.lower() == "hsv": - await self.hsv_to_rgb(ctx, user_color) - elif mode.lower() == "hsl": - await self.hsl_to_rgb(ctx, user_color) - elif mode.lower() == "cmyk": - await self.cmyk_to_rgb(ctx, user_color) - elif mode.lower() == "name": - color_name, hex_color = self.match_color_name(user_color) - if "#" in hex_color: - rgb_color = ImageColor.getcolor(hex_color, "RGB") - else: - rgb_color = ImageColor.getcolor("#" + hex_color, "RGB") - await self.color_embed(ctx, rgb_color, color_name) - else: - # mode is either None or an invalid code - if mode is None: - no_mode_embed = Embed( - title="No mode was passed, please define a color code.", - description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.", - color=Colours.soft_red, - ) - await ctx.send(embed=no_mode_embed) - return - wrong_mode_embed = Embed( - title=f"The color code {mode} is not a valid option", - description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.", - color=Colours.soft_red, - ) - await ctx.send(embed=wrong_mode_embed) - return - - @staticmethod - def tuple_create(input_color: str) -> tuple[int, int, int]: - """ - Create a tuple of integers based on user's input. - - Can handle inputs of the types: - (100, 100, 100) - 100, 100, 100 - 100 100 100 - """ - if "(" in input_color: - remove = "[() ]" - color_tuple = re.sub(remove, "", input_color) - color_tuple = tuple(map(int, color_tuple.split(","))) - elif "," in input_color: - color_tuple = tuple(map(int, input_color.split(","))) - else: - color_tuple = tuple(map(int, input_color.split(" "))) - return color_tuple - - async def hex_to_rgb(self, ctx: commands.Context, hex_string: str) -> None: - """Function to convert hex color to rgb color and send main embed.""" - hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", hex_string) - if hex_match: - if "#" in hex_string: - rgb_color = ImageColor.getcolor(hex_string, "RGB") - elif "0x" in hex_string: - hex_ = hex_string.replace("0x", "#") - rgb_color = ImageColor.getcolor(hex_, "RGB") - else: - hex_ = "#" + hex_string - rgb_color = ImageColor.getcolor(hex_, "RGB") - await self.color_embed(ctx, rgb_color) - else: - await ctx.send( - embed=Embed( - title="There was an issue converting the hex color code.", - description=ERROR_MSG.format(user_color=hex_string), - ) + def _rgb_to_name(rgb: list[int]) -> str: + """Function to convert from an RGB list to a fuzzy matched color name.""" + input_hex_color = Colour._rgb_to_hex(rgb) + try: + match, certainty, _ = process.extractOne( + query=input_hex_color, + choices=COLOR_MAPPING.values(), + score_cutoff=80 ) - - async def hsv_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int]) -> tuple[int, int, int]: - """Function to convert hsv color to rgb color and send main embed.""" - input_color = self.tuple_create(input_color) - (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped - h = h / 360 - s = s / 100 - v = v / 100 - rgb_color = colorsys.hsv_to_rgb(h, s, v) - (r, g, b) = rgb_color - r = int(r * 255) - g = int(g * 255) - b = int(b * 255) - await self.color_embed(ctx, (r, g, b)) - - async def hsl_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int]) -> tuple[int, int, int]: - """Function to convert hsl color to rgb color and send main embed.""" - input_color = self.tuple_create(input_color) - (h, s, l) = input_color - h = h / 360 - s = s / 100 - l = l / 100 # noqa: E741 It's little `L`, Reason: To maintain consistency. - rgb_color = colorsys.hls_to_rgb(h, l, s) - (r, g, b) = rgb_color - r = int(r * 255) - g = int(g * 255) - b = int(b * 255) - await self.color_embed(ctx, (r, g, b)) - - async def cmyk_to_rgb( - self, - ctx: commands.Context, - input_color: tuple[int, int, int, int] - ) -> tuple[int, int, int]: - """Function to convert cmyk color to rgb color and send main embed.""" - input_color = self.tuple_create(input_color) - c = input_color[0] - m = input_color[1] - y = input_color[2] - k = input_color[3] - r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) - g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) - b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) - await self.color_embed(ctx, (r, g, b)) - - @staticmethod - async def create_thumbnail_attachment(color: tuple[int, int, int]) -> File: - """ - Generate a thumbnail from `color`. - - Assumes that color is an rgb tuple. - """ - thumbnail = Image.new("RGB", (80, 80), color=color) - bufferedio = BytesIO() - thumbnail.save(bufferedio, format="PNG") - bufferedio.seek(0) - - file = File(bufferedio, filename="color.png") - - return file - - @staticmethod - def get_color_fields(rgb_color: tuple[int, int, int]) -> list[dict]: - """Converts from `RGB` to `CMYK`, `HSV`, `HSL` and returns a list of fields.""" - - def _rgb_to_hex(rgb_color: tuple[int, int, int]) -> str: - """To convert from `RGB` to `Hex` notation.""" - return '#' + ''.join(hex(int(color))[2:].zfill(2) for color in rgb_color).upper() - - def _rgb_to_cmyk(rgb_color: tuple[int, int, int]) -> tuple[int, int, int, int]: - """To convert from `RGB` to `CMYK` color space.""" - r, g, b = rgb_color - - # RGB_SCALE -> 255 - # CMYK_SCALE -> 100 - - if (r == g == b == 0): - return 0, 0, 0, 100 # Representing Black - - # rgb [0,RGB_SCALE] -> cmy [0,1] - c = 1 - r / 255 - m = 1 - g / 255 - y = 1 - b / 255 - - # extract out k [0, 1] - min_cmy = min(c, m, y) - c = (c - min_cmy) / (1 - min_cmy) - m = (m - min_cmy) / (1 - min_cmy) - y = (y - min_cmy) / (1 - min_cmy) - k = min_cmy - - # rescale to the range [0,CMYK_SCALE] and round off - c = round(c * 100) - m = round(m * 100) - y = round(y * 100) - k = round(k * 100) - - return c, m, y, k - - def _rgb_to_hsv(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: - """To convert from `RGB` to `HSV` color space.""" - r, g, b = rgb_color - h, v, s = colorsys.rgb_to_hsv(r / float(255), g / float(255), b / float(255)) - h = round(h * 360) - s = round(s * 100) - v = round(v * 100) - return h, s, v - - def _rgb_to_hsl(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: - """To convert from `RGB` to `HSL` color space.""" - r, g, b = rgb_color - h, l, s = colorsys.rgb_to_hls(r / float(255), g / float(255), b / float(255)) - h = round(h * 360) - s = round(s * 100) - l = round(l * 100) # noqa: E741 It's little `L`, Reason: To maintain consistency. - return h, s, l - - all_fields = [ - { - "name": "RGB", - "value": f"» rgb {rgb_color}" - }, - { - "name": "HEX", - "value": f"» hex {_rgb_to_hex(rgb_color)}" - }, - { - "name": "CMYK", - "value": f"» cmyk {_rgb_to_cmyk(rgb_color)}" - }, - { - "name": "HSV", - "value": f"» hsv {_rgb_to_hsv(rgb_color)}" - }, - { - "name": "HSL", - "value": f"» hsl {_rgb_to_hsl(rgb_color)}" - }, - ] - - return all_fields + logger.debug(f"{match = }, {certainty = }") + color_name = [name for name, _ in COLOR_MAPPING.items() if _ == match][0] + logger.debug(f"{color_name = }") + except TypeError: + color_name = None + return color_name @staticmethod def match_color_name(input_color_name: str) -> str: @@ -406,66 +195,14 @@ class Color(commands.Cog): score_cutoff=50 ) logger.debug(f"{match = }, {certainty = }") - hex_match = COLOR_MAPPING[match] + hex_match = f"#{COLOR_MAPPING[match]}" logger.debug(f"{hex_match = }") except TypeError: match = "No color name match found." hex_match = input_color_name - return match, hex_match - @staticmethod - def match_color_hex(input_hex_color: str) -> str: - """Use fuzzy matching to return a hex color code based on the user's input.""" - try: - match, certainty, _ = process.extractOne( - query=input_hex_color, - choices=COLOR_MAPPING.values(), - score_cutoff=80 - ) - logger.debug(f"{match = }, {certainty = }") - color_name = [name for name, _ in COLOR_MAPPING.items() if _ == match][0] - logger.debug(f"{color_name = }") - except TypeError: - color_name = "No color name match found." - - return color_name - - async def color_embed( - self, - ctx: commands.Context, - rgb_color: tuple[int, int, int], - color_name: str = None - ) -> None: - """Take a RGB color tuple, create embed, and send.""" - (r, g, b) = rgb_color - discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) - all_colors = self.get_color_fields(rgb_color) - hex_color = all_colors[1]["value"].replace("» hex ", "") - if color_name is None: - logger.debug(f"Find color name from hex color: {hex_color}") - color_name = self.match_color_hex(hex_color) - - async with ctx.typing(): - main_embed = Embed( - title=color_name, - description='(Approx..)', - color=discord_rgb_int, - ) - - file = await self.create_thumbnail_attachment(rgb_color) - main_embed.set_thumbnail(url="attachment://color.png") - - for field in all_colors: - main_embed.add_field( - name=field['name'], - value=field['value'], - inline=False, - ) - - await ctx.send(file=file, embed=main_embed) - def setup(bot: Bot) -> None: - """Load the Color Cog.""" - bot.add_cog(Color(bot)) + """Load the Colour cog.""" + bot.add_cog(Colour(bot)) |