Skip to content

Instantly share code, notes, and snippets.

@akingdom
Last active September 26, 2026 11:32
Show Gist options
  • Select an option

  • Save akingdom/0a0edd3ea37a9a331983cff3a69c4bee to your computer and use it in GitHub Desktop.

Select an option

Save akingdom/0a0edd3ea37a9a331983cff3a69c4bee to your computer and use it in GitHub Desktop.
Converts colours between web hex color and an RGBA object.
// 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
// 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
# 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