Last active
September 26, 2026 11:32
-
-
Save akingdom/0a0edd3ea37a9a331983cff3a69c4bee to your computer and use it in GitHub Desktop.
Converts colours between web hex color and an RGBA object.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Javascript | |
| // Color from a web-style hex string -- for named colors, see https://gist.github.com/akingdom/0a0edd3ea37a9a331983cff3a69c4bee | |
| // By Andrew Kingdom | |
| // MIT license (use free and do not remove this author's name, etc) | |
| // | |
| // **Supported string formats**: | |
| // - `"fff" // RGB` | |
| // - `"#fff" // #RGB` | |
| // - `"ffff" // RGBA` | |
| // - `"#ffff" // #RGBA` | |
| // - `"ffffff" // RRGGBB` | |
| // - `"#ffffff" // #RRGGBB` | |
| // - `"ffffffff" // RRGGBBAA` | |
| // - `"#ffffffff" // #RRGGBBAA` | |
| // | |
| // The digits represent **R**ed, **G**reen, **B**lue and **A**lpha (like transparency). | |
| // | |
| function colorfromHex(hex) { | |
| var r = 0, g = 0, b = 0, a = 255; | |
| var offset = hex.startsWith("#") ? 1 : 0; | |
| if (hex.length == 4 + offset) { | |
| r = parseInt(hex[offset] + hex[offset], 16); | |
| g = parseInt(hex[offset+1] + hex[offset+1], 16); | |
| b = parseInt(hex[offset+2] + hex[offset+2], 16); | |
| a = parseInt(hex[offset+3] + hex[offset+3], 16); | |
| } else if (hex.length == 8 + offset) { | |
| a = parseInt(hex[offset+6] + hex[offset+7], 16); | |
| r = parseInt(hex[offset] + hex[offset+1], 16); | |
| g = parseInt(hex[offset+2] + hex[offset+3], 16); | |
| b = parseInt(hex[offset+4] + hex[offset+5], 16); | |
| } else if (hex.length == 6 + offset) { | |
| r = parseInt(hex[offset] + hex[offset+1], 16); | |
| g = parseInt(hex[offset+2] + hex[offset+3], 16); | |
| b = parseInt(hex[offset+4] + hex[offset+5], 16); | |
| } else { | |
| r = g = b = a = 0; | |
| } | |
| return { r: r, g: g, b: b, a: a }; | |
| } | |
| function RGBAToHex({r, g, b, a = 255}) { | |
| const toHex = (n) => n.toString(16).padStart(2, '0'); | |
| return `#${toHex(r)}${toHex(g)}${toHex(b)}${a < 255 ? toHex(a) : ''}`; | |
| } | |
| // Example usage: | |
| console.log(colorfromHex("#ff00ff")); // { r: 255, g: 0, b: 255, a: 255 } | |
| const color = { r: 255, g: 0, b: 255, a: 255 }; | |
| console.log("Hex:", RGBAToHex(color)); // #ff00ff |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Javascript | |
| // Color from a web-style hex string with named colors | |
| // By Andrew Kingdom | |
| // MIT license (use free and do not remove this author's name, etc) | |
| // | |
| // **Supported string formats**: | |
| // - `"fff" // RGB` | |
| // - `"#fff" // #RGB` | |
| // - `"ffff" // RGBA` | |
| // - `"#ffff" // #RGBA` | |
| // - `"ffffff" // RRGGBB` | |
| // - `"#ffffff" // #RRGGBB` | |
| // - `"ffffffff" // RRGGBBAA` | |
| // - `"#ffffffff" // #RRGGBBAA` | |
| // | |
| // The digits represent **R**ed, **G**reen, **B**lue and **A**lpha (like transparency). | |
| // | |
| // Javascript | |
| // Color from a web-style hex string with named colors | |
| // By Andrew Kingdom | |
| // MIT license (use free and do not remove this author's name, etc) | |
| // | |
| // **Supported string formats**: | |
| // - `"fff" // RGB` | |
| // - `"#fff" // #RGB` | |
| // - `"ffff" // RGBA` | |
| // - `"#ffff" // #RGBA` | |
| // - `"ffffff" // RRGGBB` | |
| // - `"#ffffff" // #RRGGBB` | |
| // - `"ffffffff" // RRGGBBAA` | |
| // - `"#ffffffff" // #RRGGBBAA` | |
| // | |
| // The digits represent **R**ed, **G**reen, **B**lue and **A**lpha (like transparency). | |
| // | |
| const namedWebColors = { | |
| black: "000000", | |
| silver: "c0c0c0", | |
| gray: "808080", | |
| white: "ffffff", | |
| maroon: "800000", | |
| red: "ff0000", | |
| purple: "800080", | |
| fuchsia: "ff00ff", | |
| green: "008000", | |
| lime: "00ff00", | |
| olive: "808000", | |
| yellow: "ffff00", | |
| navy: "000080", | |
| blue: "0000ff", | |
| teal: "008080", | |
| aqua: "00ffff", | |
| aliceblue: "f0f8ff", | |
| antiquewhite: "faebd7", | |
| aquamarine: "7fffd4", | |
| azure: "f0ffff", | |
| beige: "f5f5dc", | |
| bisque: "ffe4c4", | |
| blanchedalmond: "ffebcd", | |
| blueviolet: "8a2be2", | |
| brown: "a52a2a", | |
| burlywood: "deb887", | |
| cadetblue: "5f9ea0", | |
| chartreuse: "7fff00", | |
| chocolate: "d2691e", | |
| coral: "ff7f50", | |
| cornflowerblue: "6495ed", | |
| cornsilk: "fff8dc", | |
| crimson: "dc143c", | |
| cyan: "00ffff", | |
| darkblue: "00008b", | |
| darkcyan: "008b8b", | |
| darkgoldenrod: "b8860b", | |
| darkgray: "a9a9a9", | |
| darkgreen: "006400", | |
| darkgrey: "a9a9a9", | |
| darkkhaki: "bdb76b", | |
| darkmagenta: "8b008b", | |
| darkolivegreen: "556b2f", | |
| darkorange: "ff8c00", | |
| darkorchid: "9932cc", | |
| darkred: "8b0000", | |
| darksalmon: "e9967a", | |
| darkseagreen: "8fbc8f", | |
| darkslateblue: "483d8b", | |
| darkslategray: "2f4f4f", | |
| darkslategrey: "2f4f4f", | |
| darkturquoise: "00ced1", | |
| darkviolet: "9400d3", | |
| deeppink: "ff1493", | |
| deepskyblue: "00bfff", | |
| dimgray: "696969", | |
| dimgrey: "696969", | |
| dodgerblue: "1e90ff", | |
| firebrick: "b22222", | |
| floralwhite: "fffaf0", | |
| forestgreen: "228b22", | |
| gainsboro: "dcdcdc", | |
| ghostwhite: "f8f8ff", | |
| gold: "ffd700", | |
| goldenrod: "daa520", | |
| greenyellow: "adff2f", | |
| honeydew: "f0fff0", | |
| hotpink: "ff69b4", | |
| indianred: "cd5c5c", | |
| indigo: "4b0082", | |
| ivory: "fffff0", | |
| khaki: "f0e68c", | |
| lavender: "e6e6fa", | |
| lavenderblush: "fff0f5", | |
| lawngreen: "7cfc00", | |
| lemonchiffon: "fffacd", | |
| lightblue: "add8e6", | |
| lightcoral: "f08080", | |
| lightcyan: "e0ffff", | |
| lightgoldenrodyellow: "fafad2", | |
| lightgray: "d3d3d3", | |
| lightgreen: "90ee90", | |
| lightgrey: "d3d3d3", | |
| lightpink: "ffb6c1", | |
| lightsalmon: "ffa07a", | |
| lightseagreen: "20b2aa", | |
| lightskyblue: "87cefa", | |
| lightslategray: "778899", | |
| lightslategrey: "778899", | |
| lightsteelblue: "b0c4de", | |
| lightyellow: "ffffe0", | |
| limegreen: "32cd32", | |
| linen: "faf0e6", | |
| magenta: "ff00ff", | |
| mediumaquamarine: "66cdaa", | |
| mediumblue: "0000cd", | |
| mediumorchid: "ba55d3", | |
| mediumpurple: "9370db", | |
| mediumseagreen: "3cb371", | |
| mediumslateblue: "7b68ee", | |
| mediumspringgreen: "00fa9a", | |
| mediumturquoise: "48d1cc", | |
| mediumvioletred: "c71585", | |
| midnightblue: "191970", | |
| mintcream: "f5fffa", | |
| mistyrose: "ffe4e1", | |
| moccasin: "ffe4b5", | |
| navajowhite: "ffdead", | |
| oldlace: "fdf5e6", | |
| olivedrab: "6b8e23", | |
| orange: "ffa500", | |
| orangered: "ff4500", | |
| orchid: "da70d6", | |
| palegoldenrod: "eee8aa", | |
| palegreen: "98fb98", | |
| paleturquoise: "afeeee", | |
| palevioletred: "db7093", | |
| papayawhip: "ffefd5", | |
| peachpuff: "ffdab9", | |
| peru: "cd853f", | |
| pink: "ffc0cb", | |
| plum: "dda0dd", | |
| powderblue: "b0e0e6", | |
| rebeccapurple: "663399", | |
| rosybrown: "bc8f8f", | |
| royalblue: "4169e1", | |
| saddlebrown: "8b4513", | |
| salmon: "fa8072", | |
| sandybrown: "f4a460", | |
| seagreen: "2e8b57", | |
| seashell: "fff5ee", | |
| sienna: "a0522d", | |
| skyblue: "87ceeb", | |
| slateblue: "6a5acd", | |
| slategray: "708090", | |
| slategrey: "708090", | |
| snow: "fffafa", | |
| springgreen: "00ff7f", | |
| steelblue: "4682b4", | |
| tan: "d2b48c", | |
| thistle: "d8bfd8", | |
| tomato: "ff6347", | |
| transparent: "00000000", | |
| turquoise: "40e0d0", | |
| violet: "ee82ee", | |
| wheat: "f5deb3", | |
| whitesmoke: "f5f5f5", | |
| yellowgreen: "9acd32" | |
| }; | |
| function colorfromHex(hex) { | |
| if (namedWebColors[hex.toLowerCase()]) { | |
| hex = namedWebColors[hex.toLowerCase()]; | |
| } | |
| var r = 0, g = 0, b = 0, a = 255; | |
| var offset = hex.startsWith("#") ? 1 : 0; | |
| if (hex.length == 4 + offset) { | |
| r = parseInt(hex[offset] + hex[offset], 16); | |
| g = parseInt(hex[offset+1] + hex[offset+1], 16); | |
| b = parseInt(hex[offset+2] + hex[offset+2], 16); | |
| a = parseInt(hex[offset+3] + hex[offset+3], 16); | |
| } else if (hex.length == 8 + offset) { | |
| a = parseInt(hex[offset+6] + hex[offset+7], 16); | |
| r = parseInt(hex[offset] + hex[offset+1], 16); | |
| g = parseInt(hex[offset+2] + hex[offset+3], 16); | |
| b = parseInt(hex[offset+4] + hex[offset+5], 16); | |
| } else if (hex.length == 6 + offset) { | |
| r = parseInt(hex[offset] + hex[offset+1], 16); | |
| g = parseInt(hex[offset+2] + hex[offset+3], 16); | |
| b = parseInt(hex[offset+4] + hex[offset+5], 16); | |
| } else { | |
| r = g = b = a = 0; | |
| } | |
| return { r: r, g: g, b: b, a: a }; | |
| } | |
| function RGBAToHex({r, g, b, a = 255}) { | |
| const toHex = (n) => n.toString(16).padStart(2, '0'); | |
| return `#${toHex(r)}${toHex(g)}${toHex(b)}${a < 255 ? toHex(a) : ''}`; | |
| } | |
| function RGBAColorDifference(col1, col2) { | |
| return Math.sqrt( | |
| (Math.abs(col1.r - col2.r) + 0.64) ** 2 + | |
| (Math.abs(col1.g - col2.g) + 0.32) ** 2 + | |
| (Math.abs(col1.b - col2.b) + 0.02) ** 2 | |
| ); | |
| } | |
| function getClosestNamedColor({r, g, b, a = 255}) { | |
| let closestColor = ''; | |
| let smallestDiff = Infinity; | |
| Object.keys(namedWebColors).forEach(name => { | |
| const colorHex = namedWebColors[name]; | |
| const color = colorfromHex(colorHex); | |
| const diff = RGBAColorDifference({ r, g, b }, color); | |
| if (diff < smallestDiff) { | |
| smallestDiff = diff; | |
| closestColor = name; | |
| } | |
| }); | |
| return closestColor; | |
| } | |
| // Example usage: | |
| console.log(colorfromHex("#ff00ff")); // { r: 255, g: 0, b: 255, a: 255 } | |
| console.log(colorfromHex("blue")); // { r: 0, g: 0, b: 255, a: 255 } | |
| const color = { r: 255, g: 0, b: 255, a: 255 }; | |
| console.log("Hex:", RGBAToHex(color)); // #ff00ff | |
| console.log("Closest Named Color:", getClosestNamedColor(color)); // fuchia |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # colour_bridge.py | |
| """Colour conversion utilities. | |
| Bridges the three colour spaces we care about: | |
| - hex string : as stored in visual_sig.hex and as the AI reports colours | |
| - RGB tuple : as PIL gives us | |
| - LAB tuple : as visual_sig.lab stores, the perceptual space | |
| Includes get_closest_named_color(), which maps any input to the nearest | |
| CSS/HTML named colour using perceptual (LAB) distance, not RGB Euclidean. | |
| RGB<->hex logic mirrors the gist by Andrew Kingdom | |
| (https://gist.github.com/akingdom/0a0edd3ea37a9a331983cff3a69c4bee). | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import re | |
| from typing import Dict, Optional, Tuple | |
| RGB = Tuple[int, int, int] | |
| LAB = Tuple[float, float, float] | |
| _HEX_RE = re.compile(r"^#?([0-9a-fA-F]{3,8})$") | |
| # ---------------------------------------------------------------------- | |
| # Named colours (the gist's table, alpha entry removed) | |
| # ---------------------------------------------------------------------- | |
| NAMED_WEB_COLORS: Dict[str, str] = { | |
| "black": "000000", "silver": "c0c0c0", "gray": "808080", "white": "ffffff", | |
| "maroon": "800000", "red": "ff0000", "purple": "800080", "fuchsia": "ff00ff", | |
| "green": "008000", "lime": "00ff00", "olive": "808000", "yellow": "ffff00", | |
| "navy": "000080", "blue": "0000ff", "teal": "008080", "aqua": "00ffff", | |
| "aliceblue": "f0f8ff", "antiquewhite": "faebd7", "aquamarine": "7fffd4", | |
| "azure": "f0ffff", "beige": "f5f5dc", "bisque": "ffe4c4", | |
| "blanchedalmond": "ffebcd", "blueviolet": "8a2be2", "brown": "a52a2a", | |
| "burlywood": "deb887", "cadetblue": "5f9ea0", "chartreuse": "7fff00", | |
| "chocolate": "d2691e", "coral": "ff7f50", "cornflowerblue": "6495ed", | |
| "cornsilk": "fff8dc", "crimson": "dc143c", "cyan": "00ffff", | |
| "darkblue": "00008b", "darkcyan": "008b8b", "darkgoldenrod": "b8860b", | |
| "darkgray": "a9a9a9", "darkgreen": "006400", "darkgrey": "a9a9a9", | |
| "darkkhaki": "bdb76b", "darkmagenta": "8b008b", "darkolivegreen": "556b2f", | |
| "darkorange": "ff8c00", "darkorchid": "9932cc", "darkred": "8b0000", | |
| "darksalmon": "e9967a", "darkseagreen": "8fbc8f", "darkslateblue": "483d8b", | |
| "darkslategray": "2f4f4f", "darkslategrey": "2f4f4f", | |
| "darkturquoise": "00ced1", "darkviolet": "9400d3", "deeppink": "ff1493", | |
| "deepskyblue": "00bfff", "dimgray": "696969", "dimgrey": "696969", | |
| "dodgerblue": "1e90ff", "firebrick": "b22222", "floralwhite": "fffaf0", | |
| "forestgreen": "228b22", "gainsboro": "dcdcdc", "ghostwhite": "f8f8ff", | |
| "gold": "ffd700", "goldenrod": "daa520", "greenyellow": "adff2f", | |
| "honeydew": "f0fff0", "hotpink": "ff69b4", "indianred": "cd5c5c", | |
| "indigo": "4b0082", "ivory": "fffff0", "khaki": "f0e68c", | |
| "lavender": "e6e6fa", "lavenderblush": "fff0f5", "lawngreen": "7cfc00", | |
| "lemonchiffon": "fffacd", "lightblue": "add8e6", "lightcoral": "f08080", | |
| "lightcyan": "e0ffff", "lightgoldenrodyellow": "fafad2", | |
| "lightgray": "d3d3d3", "lightgreen": "90ee90", "lightgrey": "d3d3d3", | |
| "lightpink": "ffb6c1", "lightsalmon": "ffa07a", "lightseagreen": "20b2aa", | |
| "lightskyblue": "87cefa", "lightslategray": "778899", | |
| "lightslategrey": "778899", "lightsteelblue": "b0c4de", | |
| "lightyellow": "ffffe0", "limegreen": "32cd32", "linen": "faf0e6", | |
| "magenta": "ff00ff", "mediumaquamarine": "66cdaa", "mediumblue": "0000cd", | |
| "mediumorchid": "ba55d3", "mediumpurple": "9370db", | |
| "mediumseagreen": "3cb371", "mediumslateblue": "7b68ee", | |
| "mediumspringgreen": "00fa9a", "mediumturquoise": "48d1cc", | |
| "mediumvioletred": "c71585", "midnightblue": "191970", | |
| "mintcream": "f5fffa", "mistyrose": "ffe4e1", "moccasin": "ffe4b5", | |
| "navajowhite": "ffdead", "oldlace": "fdf5e6", "olivedrab": "6b8e23", | |
| "orange": "ffa500", "orangered": "ff4500", "orchid": "da70d6", | |
| "palegoldenrod": "eee8aa", "palegreen": "98fb98", "paleturquoise": "afeeee", | |
| "palevioletred": "db7093", "papayawhip": "ffefd5", "peachpuff": "ffdab9", | |
| "peru": "cd853f", "pink": "ffc0cb", "plum": "dda0dd", | |
| "powderblue": "b0e0e6", "rebeccapurple": "663399", "rosybrown": "bc8f8f", | |
| "royalblue": "4169e1", "saddlebrown": "8b4513", "salmon": "fa8072", | |
| "sandybrown": "f4a460", "seagreen": "2e8b57", "seashell": "fff5ee", | |
| "sienna": "a0522d", "skyblue": "87ceeb", "slateblue": "6a5acd", | |
| "slategray": "708090", "slategrey": "708090", "snow": "fffafa", | |
| "springgreen": "00ff7f", "steelblue": "4682b4", "tan": "d2b48c", | |
| "thistle": "d8bfd8", "tomato": "ff6347", "turquoise": "40e0d0", | |
| "violet": "ee82ee", "wheat": "f5deb3", "whitesmoke": "f5f5f5", | |
| "yellowgreen": "9acd32", | |
| } | |
| # ---------------------------------------------------------------------- | |
| # Hex <-> RGB | |
| # ---------------------------------------------------------------------- | |
| def hex_to_rgb(value: str) -> Optional[RGB]: | |
| """Parse any of fff, #fff, ffff, #ffff, ffffff, #ffffff, ffffffff, #ffffffff. | |
| Alpha is parsed but discarded. Returns None on unrecognised input. | |
| """ | |
| if not isinstance(value, str): | |
| return None | |
| s = value.strip().lower() | |
| # Named colour? | |
| if s in NAMED_WEB_COLORS: | |
| s = NAMED_WEB_COLORS[s] | |
| m = _HEX_RE.match(s) | |
| if not m: | |
| return None | |
| h = m.group(1) | |
| if len(h) == 3: | |
| r, g, b = (int(c * 2, 16) for c in h) | |
| elif len(h) == 4: # RGBA shorthand | |
| r, g, b = (int(c * 2, 16) for c in h[:3]) | |
| elif len(h) == 6: | |
| r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) | |
| elif len(h) == 8: # RRGGBBAA | |
| r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) | |
| else: | |
| return None | |
| return (r, g, b) | |
| def rgb_to_hex(rgb: RGB) -> str: | |
| r, g, b = rgb | |
| return f"#{r:02x}{g:02x}{b:02x}" | |
| def name_to_rgb(name: str) -> Optional[RGB]: | |
| """Look up a CSS/HTML named colour. Case-insensitive.""" | |
| if not isinstance(name, str): | |
| return None | |
| h = NAMED_WEB_COLORS.get(name.strip().lower()) | |
| if h is None: | |
| return None | |
| return hex_to_rgb(h) | |
| # ---------------------------------------------------------------------- | |
| # RGB <-> LAB (sRGB D65, same reference as image_bridge_toolkit) | |
| # ---------------------------------------------------------------------- | |
| def _srgb_to_linear(c: float) -> float: | |
| return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 | |
| def _linear_to_srgb(c: float) -> float: | |
| return 12.92 * c if c <= 0.0031308 else 1.055 * (c ** (1 / 2.4)) - 0.055 | |
| def rgb_to_lab(rgb: RGB) -> LAB: | |
| r, g, b = (c / 255.0 for c in rgb) | |
| r, g, b = _srgb_to_linear(r), _srgb_to_linear(g), _srgb_to_linear(b) | |
| x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375 | |
| y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750 | |
| z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041 | |
| xn, yn, zn = 0.95047, 1.0, 1.08883 | |
| x, y, z = x / xn, y / yn, z / zn | |
| def f(t: float) -> float: | |
| return t ** (1 / 3) if t > 0.008856 else 7.787 * t + 16 / 116 | |
| fx, fy, fz = f(x), f(y), f(z) | |
| return (116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)) | |
| def lab_to_rgb(lab: LAB) -> RGB: | |
| L, a, b = lab | |
| fy = (L + 16) / 116 | |
| fx = fy + a / 500 | |
| fz = fy - b / 200 | |
| def f_inv(t: float) -> float: | |
| return t ** 3 if t ** 3 > 0.008856 else (t - 16 / 116) / 7.787 | |
| x = f_inv(fx) * 0.95047 | |
| y = f_inv(fy) * 1.0 | |
| z = f_inv(fz) * 1.08883 | |
| r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314 | |
| g = x * -0.9692660 + y * 1.8760108 + z * 0.0415560 | |
| bb = x * 0.0556434 + y * -0.2040259 + z * 1.0572252 | |
| r, g, bb = _linear_to_srgb(r), _linear_to_srgb(g), _linear_to_srgb(bb) | |
| return ( | |
| max(0, min(255, round(r * 255))), | |
| max(0, min(255, round(g * 255))), | |
| max(0, min(255, round(bb * 255))), | |
| ) | |
| # ---------------------------------------------------------------------- | |
| # Distance | |
| # ---------------------------------------------------------------------- | |
| def lab_distance(a: LAB, b: LAB) -> float: | |
| """Euclidean distance in LAB. 0 = identical. | |
| Roughly: <5 barely perceptible, >25 clearly different colour.""" | |
| return math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b))) | |
| def hex_distance(a_hex: str, b_hex: str) -> Optional[float]: | |
| a = hex_to_rgb(a_hex) | |
| b = hex_to_rgb(b_hex) | |
| if a is None or b is None: | |
| return None | |
| return lab_distance(rgb_to_lab(a), rgb_to_lab(b)) | |
| # ---------------------------------------------------------------------- | |
| # Closest named colour | |
| # ---------------------------------------------------------------------- | |
| # Precompute LAB for every named colour once, at import. | |
| _NAMED_LAB: Dict[str, LAB] = {} | |
| for _name, _hex in NAMED_WEB_COLORS.items(): | |
| _rgb = hex_to_rgb(_hex) | |
| if _rgb is not None: | |
| _NAMED_LAB[_name] = rgb_to_lab(_rgb) | |
| def get_closest_named_color(value) -> Tuple[str, float]: | |
| """Return (name, distance) for the nearest CSS named colour. | |
| `value` may be a hex string (with or without '#'), a named colour name, | |
| an (r, g, b) tuple, or an (L, a, b) tuple. Returns ('', inf) if the | |
| input can't be parsed. | |
| Distance is in LAB units; see lab_distance() for scale. | |
| """ | |
| lab = _to_lab(value) | |
| if lab is None: | |
| return ("", float("inf")) | |
| best_name = "" | |
| best_dist = float("inf") | |
| for name, ref_lab in _NAMED_LAB.items(): | |
| d = lab_distance(lab, ref_lab) | |
| if d < best_dist: | |
| best_dist = d | |
| best_name = name | |
| return (best_name, round(best_dist, 3)) | |
| def _to_lab(value) -> Optional[LAB]: | |
| """Coerce whatever the caller gave us into a LAB tuple.""" | |
| if value is None: | |
| return None | |
| # (r, g, b) tuple of ints 0-255 | |
| if isinstance(value, (tuple, list)) and len(value) == 3: | |
| try: | |
| rgb = (int(value[0]), int(value[1]), int(value[2])) | |
| except (TypeError, ValueError): | |
| return None | |
| # Ambiguous: could be LAB passed as a tuple. Heuristic: if L in 0-100, | |
| # a/b in -128..127 and the caller used floats, treat as LAB. | |
| if all(isinstance(v, float) for v in value): | |
| L, a, b = float(value[0]), float(value[1]), float(value[2]) | |
| if -5 <= L <= 105 and -130 <= a <= 130 and -130 <= b <= 130: | |
| return (L, a, b) | |
| if all(0 <= c <= 255 for c in rgb): | |
| return rgb_to_lab(rgb) | |
| return None | |
| if isinstance(value, str): | |
| rgb = hex_to_rgb(value) | |
| if rgb is None: | |
| return None | |
| return rgb_to_lab(rgb) | |
| return None |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment