Skip to content

Instantly share code, notes, and snippets.

@gagbo
Created July 31, 2019 20:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gagbo/7943c9a71cab22b641d2904a6d59ec3a to your computer and use it in GitHub Desktop.
Save gagbo/7943c9a71cab22b641d2904a6d59ec3a to your computer and use it in GitHub Desktop.
Translate truecolor to 256-color palette
#!/usr/bin/env python3
"""
Convert values between RGB hex codes and xterm-256 color codes.
Nice long listing of all 256 colors and their codes. Useful for
developing console color themes, or even script output schemes.
The legacy version uses naive euclidean distance in rgb space,
the non-legacy version uses naive euclidean distance in hsv space, manually
tweaked to work better with low values colors
Thanks to Micah Elliott for the legacy version that I used in the beginning
"""
import sys
import re
from typing import Tuple, Dict
__author__ = "Gerry Agbobada (@gagbo)"
__version__ = "0.1"
__copyright__ = "Copyright (C) 2019 Gerry Agbobada. All rights reserved."
__license__ = "MIT"
# ---------------------------------------------------------------------
CLUT = [ # color look-up table
# 8-bit, RGB hex
# Primary 3-bit (8 colors). Unique representation!
# Equivalent "bright" versions of original 8 colors.
# They are removed because they usually conflict with
# Terminal emulators colorschemes
# Strictly ascending.
("16", "000000"),
("17", "00005f"),
("18", "000087"),
("19", "0000af"),
("20", "0000d7"),
("21", "0000ff"),
("22", "005f00"),
("23", "005f5f"),
("24", "005f87"),
("25", "005faf"),
("26", "005fd7"),
("27", "005fff"),
("28", "008700"),
("29", "00875f"),
("30", "008787"),
("31", "0087af"),
("32", "0087d7"),
("33", "0087ff"),
("34", "00af00"),
("35", "00af5f"),
("36", "00af87"),
("37", "00afaf"),
("38", "00afd7"),
("39", "00afff"),
("40", "00d700"),
("41", "00d75f"),
("42", "00d787"),
("43", "00d7af"),
("44", "00d7d7"),
("45", "00d7ff"),
("46", "00ff00"),
("47", "00ff5f"),
("48", "00ff87"),
("49", "00ffaf"),
("50", "00ffd7"),
("51", "00ffff"),
("52", "5f0000"),
("53", "5f005f"),
("54", "5f0087"),
("55", "5f00af"),
("56", "5f00d7"),
("57", "5f00ff"),
("58", "5f5f00"),
("59", "5f5f5f"),
("60", "5f5f87"),
("61", "5f5faf"),
("62", "5f5fd7"),
("63", "5f5fff"),
("64", "5f8700"),
("65", "5f875f"),
("66", "5f8787"),
("67", "5f87af"),
("68", "5f87d7"),
("69", "5f87ff"),
("70", "5faf00"),
("71", "5faf5f"),
("72", "5faf87"),
("73", "5fafaf"),
("74", "5fafd7"),
("75", "5fafff"),
("76", "5fd700"),
("77", "5fd75f"),
("78", "5fd787"),
("79", "5fd7af"),
("80", "5fd7d7"),
("81", "5fd7ff"),
("82", "5fff00"),
("83", "5fff5f"),
("84", "5fff87"),
("85", "5fffaf"),
("86", "5fffd7"),
("87", "5fffff"),
("88", "870000"),
("89", "87005f"),
("90", "870087"),
("91", "8700af"),
("92", "8700d7"),
("93", "8700ff"),
("94", "875f00"),
("95", "875f5f"),
("96", "875f87"),
("97", "875faf"),
("98", "875fd7"),
("99", "875fff"),
("100", "878700"),
("101", "87875f"),
("102", "878787"),
("103", "8787af"),
("104", "8787d7"),
("105", "8787ff"),
("106", "87af00"),
("107", "87af5f"),
("108", "87af87"),
("109", "87afaf"),
("110", "87afd7"),
("111", "87afff"),
("112", "87d700"),
("113", "87d75f"),
("114", "87d787"),
("115", "87d7af"),
("116", "87d7d7"),
("117", "87d7ff"),
("118", "87ff00"),
("119", "87ff5f"),
("120", "87ff87"),
("121", "87ffaf"),
("122", "87ffd7"),
("123", "87ffff"),
("124", "af0000"),
("125", "af005f"),
("126", "af0087"),
("127", "af00af"),
("128", "af00d7"),
("129", "af00ff"),
("130", "af5f00"),
("131", "af5f5f"),
("132", "af5f87"),
("133", "af5faf"),
("134", "af5fd7"),
("135", "af5fff"),
("136", "af8700"),
("137", "af875f"),
("138", "af8787"),
("139", "af87af"),
("140", "af87d7"),
("141", "af87ff"),
("142", "afaf00"),
("143", "afaf5f"),
("144", "afaf87"),
("145", "afafaf"),
("146", "afafd7"),
("147", "afafff"),
("148", "afd700"),
("149", "afd75f"),
("150", "afd787"),
("151", "afd7af"),
("152", "afd7d7"),
("153", "afd7ff"),
("154", "afff00"),
("155", "afff5f"),
("156", "afff87"),
("157", "afffaf"),
("158", "afffd7"),
("159", "afffff"),
("160", "d70000"),
("161", "d7005f"),
("162", "d70087"),
("163", "d700af"),
("164", "d700d7"),
("165", "d700ff"),
("166", "d75f00"),
("167", "d75f5f"),
("168", "d75f87"),
("169", "d75faf"),
("170", "d75fd7"),
("171", "d75fff"),
("172", "d78700"),
("173", "d7875f"),
("174", "d78787"),
("175", "d787af"),
("176", "d787d7"),
("177", "d787ff"),
("178", "d7af00"),
("179", "d7af5f"),
("180", "d7af87"),
("181", "d7afaf"),
("182", "d7afd7"),
("183", "d7afff"),
("184", "d7d700"),
("185", "d7d75f"),
("186", "d7d787"),
("187", "d7d7af"),
("188", "d7d7d7"),
("189", "d7d7ff"),
("190", "d7ff00"),
("191", "d7ff5f"),
("192", "d7ff87"),
("193", "d7ffaf"),
("194", "d7ffd7"),
("195", "d7ffff"),
("196", "ff0000"),
("197", "ff005f"),
("198", "ff0087"),
("199", "ff00af"),
("200", "ff00d7"),
("201", "ff00ff"),
("202", "ff5f00"),
("203", "ff5f5f"),
("204", "ff5f87"),
("205", "ff5faf"),
("206", "ff5fd7"),
("207", "ff5fff"),
("208", "ff8700"),
("209", "ff875f"),
("210", "ff8787"),
("211", "ff87af"),
("212", "ff87d7"),
("213", "ff87ff"),
("214", "ffaf00"),
("215", "ffaf5f"),
("216", "ffaf87"),
("217", "ffafaf"),
("218", "ffafd7"),
("219", "ffafff"),
("220", "ffd700"),
("221", "ffd75f"),
("222", "ffd787"),
("223", "ffd7af"),
("224", "ffd7d7"),
("225", "ffd7ff"),
("226", "ffff00"),
("227", "ffff5f"),
("228", "ffff87"),
("229", "ffffaf"),
("230", "ffffd7"),
("231", "ffffff"),
# Gray-scale range.
("232", "080808"),
("233", "121212"),
("234", "1c1c1c"),
("235", "262626"),
("236", "303030"),
("237", "3a3a3a"),
("238", "444444"),
("239", "4e4e4e"),
("240", "585858"),
("241", "626262"),
("242", "6c6c6c"),
("243", "767676"),
("244", "808080"),
("245", "8a8a8a"),
("246", "949494"),
("247", "9e9e9e"),
("248", "a8a8a8"),
("249", "b2b2b2"),
("250", "bcbcbc"),
("251", "c6c6c6"),
("252", "d0d0d0"),
("253", "dadada"),
("254", "e4e4e4"),
("255", "eeeeee"),
]
def _strip_hash(hexstring: str) -> str:
"""
Strip leading `#` if exists.
"""
if hexstring.startswith("#"):
hexstring = hexstring.lstrip("#")
return hexstring
def hex2rgb(hexstring: str) -> Tuple[int, int, int]:
"""
Break 6-char RGB code into 3 integer vals.
"""
rgb = _strip_hash(hexstring)
red_int, green_int, blue_int = [
int(h, 16) for h in re.split(r"(..)(..)(..)", rgb)[1:4]
]
return red_int, green_int, blue_int
def _hex2hsv(hexstring: str) -> Tuple[float, float, float]:
"""
Return the HSV tuple from a hex string
>>> _hex2hsv('000000')
(0.0, 0.0, 0.0)
>>> _hex2hsv('FFFFFF')
(0.0, 0.0, 1.0)
>>> _hex2hsv('FF0000')
(0.0, 1.0, 1.0)
>>> _hex2hsv('808000')
(60.0, 1.0, 0.5)
"""
rgb_int = hex2rgb(hexstring)
rgb_f = tuple(map(lambda val: float(val) / 255.0, rgb_int))
c_min = min(rgb_f)
c_max = max(rgb_f)
delta = c_max - c_min
# Hue
if delta == 0:
hue = 0.0
elif c_max == rgb_f[0]:
hue = 60.0 * ((rgb_f[1] - rgb_f[2]) / delta % 6)
elif c_max == rgb_f[1]:
hue = 60.0 * ((rgb_f[2] - rgb_f[0]) / delta + 2)
else:
hue = 60.0 * ((rgb_f[0] - rgb_f[1]) / delta + 4)
# Saturation
if c_max == 0:
sat = 0.0
else:
sat = delta / c_max
# Value
val = c_max
return (round(hue, 2), round(sat, 2), round(val, 2))
def _create_dicts() -> Tuple[
Dict[str, str],
Dict[str, str],
Dict[str, Tuple[float, float, float]],
Dict[Tuple[float, float, float], str],
]:
short2rgb_dict = dict(CLUT)
rgb2short_dict = {}
short2hsv_dict = {}
hsv2short_dict = {}
for k, v in short2rgb_dict.items():
rgb2short_dict[v] = k
hsv = _hex2hsv(v)
short2hsv_dict[k] = hsv
hsv2short_dict[hsv] = k
return rgb2short_dict, short2rgb_dict, short2hsv_dict, hsv2short_dict
def short2rgb(short: str) -> str:
return SHORT2RGB_DICT[short]
def rgb2short_legacy(hexstring: str) -> Tuple[str, str]:
"""
Find the closest xterm-256 approximation to the given RGB value.
@param hexstring: Hex code representing an RGB value, eg, 'abcdef'
@returns: String between 0 and 255, compatible with xterm.
>>> rgb2short_legacy('123456')
('23', '005f5f')
>>> rgb2short_legacy('ffffff')
('231', 'ffffff')
>>> rgb2short_legacy('0DADD6') # vimeo logo
('38', '00afd7')
"""
incs = (0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF)
# Break 6-char RGB code into 3 integer vals.
parts = hex2rgb(hexstring)
res_parts = []
for part in parts:
i = 0
while i < len(incs) - 1:
s, b = incs[i], incs[i + 1] # smaller, bigger
if s <= part <= b:
s1 = abs(s - part)
b1 = abs(b - part)
closest = s if s1 < b1 else b
res_parts.append(closest)
break
i += 1
# print '***', res
res = "".join([("%02.x" % i) for i in res_parts])
equiv = RGB2SHORT_DICT[res]
# print '***', res, equiv
return equiv, res
def rgb2short(hexstring: str) -> Tuple[str, str]:
"""
Find the closest xterm-256 approximation to the given RGB value.
@param hexstring: Hex code representing an RGB value, eg, 'abcdef'
@returns: String between 0 and 255, compatible with xterm.
>>> rgb2short('123456')
('17', '00005f')
>>> rgb2short('ffffff')
('231', 'ffffff')
>>> rgb2short('0DADD6') # vimeo logo
('38', '00afd7')
"""
hsv = _hex2hsv(hexstring)
result = hsv2short(hsv)
return result, SHORT2RGB_DICT[result]
def short2hsv(short: str) -> Tuple[float, float, float]:
return SHORT2HSV_DICT[short]
def _hsvdistance(a: Tuple[float, float, float], b: Tuple[float, float, float]) -> float:
hue_diff = ((a[0] - b[0]) + 180) % 360 - 180
hue_dist = abs(hue_diff / 360.0) ** 2
sat_dist = abs(a[1] - b[1]) ** 2
val_dist = abs(a[2] - b[2]) ** 1
return hue_dist + sat_dist + val_dist
def hsv2short(hsv: Tuple[float, float, float]) -> str:
"""
Return the xterm palette color of the closest candidate in
hue, saturation and value
"""
xterm_val, xterm_hsv = min(
SHORT2HSV_DICT.items(), key=lambda kv_pair: _hsvdistance(kv_pair[1], hsv)
)
return xterm_val
def print_all() -> None:
"""
Print all 256 xterm color codes.
"""
for short, rgb in CLUT:
sys.stdout.write("\033[48;5;%sm%s:%s" % (short, short, rgb))
sys.stdout.write("\033[0m ")
sys.stdout.write("\033[38;5;%sm%s:%s" % (short, short, rgb))
sys.stdout.write("\033[0m\n")
print("Printed all codes.")
print("You can translate a hex or 0-255 code by providing an argument.")
RGB2SHORT_DICT, SHORT2RGB_DICT, SHORT2HSV_DICT, HSV2SHORT_DICT = _create_dicts()
# ------------------------------------------------------------------
if __name__ == "__main__":
import doctest
doctest.testmod()
if len(sys.argv) == 1:
print_all()
raise SystemExit
arg = sys.argv[1]
if len(arg) < 4 and int(arg) < 256:
rgb = short2rgb(arg)
sys.stdout.write(
"xterm color \033[38;5;%sm%s\033[0m -> RGB exact "
"\033[38;5;%sm%s\033[0m" % (arg, arg, arg, rgb)
)
sys.stdout.write("\033[0m\n")
else:
arg_rgb = hex2rgb(arg)
short_legacy, rgb_legacy = rgb2short_legacy(arg)
short, rgb = rgb2short(arg)
sys.stdout.write(
"(LEGACY) RGB %s \033[38;2;%s;%s;%sm(TRUECOLOR) "
"\033[0m-> xterm color approx "
"\033[38;5;%sm%s (%s)"
% (
arg,
arg_rgb[0],
arg_rgb[1],
arg_rgb[2],
short_legacy,
short_legacy,
rgb_legacy,
)
)
sys.stdout.write("\033[0m\n")
sys.stdout.write(
"RGB %s \033[38;2;%s;%s;%sm(TRUECOLOR) "
"\033[0m-> xterm color approx "
"\033[38;5;%sm%s (%s)"
% (arg, arg_rgb[0], arg_rgb[1], arg_rgb[2], short, short, rgb)
)
sys.stdout.write("\033[0m\n")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment