This is a somewhat complicated question, see more discussion here: https://graphicdesign.stackexchange.com/questions/92984/how-can-i-tell-basic-color-a-hex-code-is-closest-to
I don't know of any library or implementation that already exists for this. If you really need this functionality though and don't need it to be insanely precise, what you can do is use a color table with the colors you want to classify, e.g.
Black #000000 (0,0,0)
White #FFFFFF (255,255,255)
Red #FF0000 (255,0,0)
Lime #00FF00 (0,255,0)
Blue #0000FF (0,0,255)
Yellow #FFFF00 (255,255,0)
Cyan / Aqua #00FFFF (0,255,255)
Magenta / Fuchsia #FF00FF (255,0,255)
Silver #C0C0C0 (192,192,192)
Gray #808080 (128,128,128)
Maroon #800000 (128,0,0)
Olive #808000 (128,128,0)
Green #008000 (0,128,0)
Purple #800080 (128,0,128)
Teal #008080 (0,128,128)
Navy #000080 (0,0,128)
(from https://www.rapidtables.com/web/color/RGB_Color.html)
And calculate which of these the given hex color is closest to. This isn't going to be completely accurate because of the way we perceive specific colors on the spectrum, but I don't think this problem has a general solution anyways so approximation may be the only path forward.
Answer from Tyler Aldrich on Stack OverflowThis is a somewhat complicated question, see more discussion here: https://graphicdesign.stackexchange.com/questions/92984/how-can-i-tell-basic-color-a-hex-code-is-closest-to
I don't know of any library or implementation that already exists for this. If you really need this functionality though and don't need it to be insanely precise, what you can do is use a color table with the colors you want to classify, e.g.
Black #000000 (0,0,0)
White #FFFFFF (255,255,255)
Red #FF0000 (255,0,0)
Lime #00FF00 (0,255,0)
Blue #0000FF (0,0,255)
Yellow #FFFF00 (255,255,0)
Cyan / Aqua #00FFFF (0,255,255)
Magenta / Fuchsia #FF00FF (255,0,255)
Silver #C0C0C0 (192,192,192)
Gray #808080 (128,128,128)
Maroon #800000 (128,0,0)
Olive #808000 (128,128,0)
Green #008000 (0,128,0)
Purple #800080 (128,0,128)
Teal #008080 (0,128,128)
Navy #000080 (0,0,128)
(from https://www.rapidtables.com/web/color/RGB_Color.html)
And calculate which of these the given hex color is closest to. This isn't going to be completely accurate because of the way we perceive specific colors on the spectrum, but I don't think this problem has a general solution anyways so approximation may be the only path forward.
I mean you could do:
hex = input()
if hex == '#0000FF':
print('Blue')
else:
print('Not blue')
If that is what you are looking for.
Yes, there are, but not all terminals support it, some do it differently than others, you need to figure out what your terminal's capabilities are... In other words: it's possible, but you don't want to do it yourself.
The standard way of doing it is hence using a library, in the Python casr that would probably be Rich or Textual, depending on whether you just want pretty colorful text or also prompts, buttons, tables etc.
Solution for feature-rich/modern terminals (emulators and virtual consoles)
All the terminals that I currently have installed (Gnome Terminal, Xterm, Alacritty, Linux's virtual console) on my system support the escaped sequences:
- Set foreground color:
\033[38;2;{r};{g};{b}m - Set background color:
\033[48;2;{r};{g};{b}m
where {r}, {g}, {b} are placeholders for the specific RGB (channel) values. (I found these escape sequences documented in this very helpful and concise Gist https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797.)
In python you can implement functions to change the foreground and background colors using these escape sequences:
SET_FOREGROUND_ESCAPE = "\033[38;2;{r};{g};{b}m"
SET_BACKGROUND_ESCAPE = "\033[48;2;{r};{g};{b}m"
def set_foreground(r, g, b):
print(SET_FOREGROUND_ESCAPE.format(r=r, g=g, b=b), end="")
def set_background(r, g, b):
print(SET_BACKGROUND_ESCAPE.format(r=r, g=g, b=b), end="")
# Example usage: print red text on a cyan background
set_foreground(255, 0, 0)
set_background( 0, 255, 255)
print("Red on Cyan")
To work with hex codes, you need a function that converts hex codes to RGB tuples:
def hex_to_rgb(rgb_hex):
# Unpack the 24-bit `rgb_hex` integer to a RGB tuple of 3 8-bit integers
return ( rgb_hex>>16 , # r is stored in the most significant 8 bits
(rgb_hex>> 8)&0b1111_1111, # g is stored in the "middle" 8 bits
rgb_hex &0b1111_1111) # b is stored in the least significant 8 bits
# Example usage
RED_HEX = 0xFF0000
CYAN_HEX = 0x00FFFF
assert hex_to_rgb(RED_HEX ) == (255, 0, 0)
assert hex_to_rgb(CYAN_HEX) == ( 0, 255, 255)
This conversion function allows us to implement the new functions set_foreground_hex and set_background_hex as counterparts to set_foreground and set_background:
def set_foreground_hex(rgb_hex):
set_foreground(*hex_to_rgb(rgb_hex))
def set_background_hex(rgb_hex):
set_background(*hex_to_rgb(rgb_hex))
Now we can set the foreground and background color using hex codes:
set_foreground_hex(0xFF0000)
set_background_hex(0x00FFFF)
print("Red on Cyan")
(You should bake rgb_to_hex and set_fore-/background directly into set_fore-/background_hex if you want performance/resource-efficiency because function call overhead is high in Python.)
Importantly, this also worked on the terminal that you get when you don't have a desktop environment installed (but the Arch Linux wiki gives me reason to believe that the kernel translates the 24-bit color to 256 color codes), meaning that this code will work in homelab/datacenter contexts, provided you are running a relatively new kernel version.
Minimal/old terminals
As pointed out in the question, the traditional way of setting the color in a terminal is to provide a color_code ranging from 0 to 255 in the escape sequences:
- Set foreground color code
\033[38;5;{color_code}m - Set background color code
\033[48;5;{color_code}m
These may be the only escape sequences supported by minimal or old terminals, despite having not encountered this situation personally.
As seen in this Wikipedia article section
- the color codes
0- 7are "Standard" colors - the color codes
8-15are "High-intensity" colors - the middle 216 codes provide more colors
- and the last 24 codes provide more grayscale shades
Assuming that we know the exact RGB values for some or all of these codes, we can convert a RGB value to a color code by finding the nearest matching code:
import math
term_color_code_rgb_values = []
def populate_term_color_code_rgb_values():
...
populate_term_color_code_rgb_values()
def calc_color_dist(r1, g1, b1, r2, g2, b2):
# Euclidean distance between <r1, g1, b1> and <r2, g2, b2>.
# Better color distance metrics exist!
r_delta = r2 - r1
g_delta = g2 - g1
b_delta = b2 - b1
return math.sqrt(r_delta*r_delta + g_delta*g_delta + b_delta*b_delta)
def find_nearest_term_color_code_from_rgb(r, g, b):
# Linearly search for the nearest color code.
# A faster algorithm could be implemented!
min_color_dist = 0xFFFF_FFFF_FFFF # arbitrary large number
nearest_color_code = -1
for color_code, (r2, g2, b2) in enumerate(term_color_code_rgb_values):
rgb_unknown = r2 == -1
if rgb_unknown:
continue
color_dist = calc_color_dist(r, g, b, r2, g2, b2)
if color_dist < min_color_dist:
min_color_dist = color_dist
nearest_color_code = color_code
return nearest_color_code
# `set_fore-/background_color_code_from_rgb_hex` use similar
# logic to the other functions for setting the fore- and background.
def set_foreground_color_code_from_rgb_hex(rgb_hex):
return print(
"\033[38;5;{color_code}m".format(
color_code=find_nearest_term_color_code_from_rgb(
rgb_hex>>16 ,
(rgb_hex>> 8)&0b1111_1111,
rgb_hex &0b1111_1111)))
def set_background_color_code_from_rgb_hex(rgb_hex):
return print(
"\033[48;5;{color_code}m".format(
color_code=find_nearest_term_color_code_from_rgb(
rgb_hex>>16 ,
(rgb_hex>> 8)&0b1111_1111,
rgb_hex &0b1111_1111)))
set_foreground_color_code_from_rgb_hex(0xFF0000)
set_background_color_code_from_rgb_hex(0x00FFFF)
print("Red on Cyan")
To complete the above code we need to implement populate_term_color_code_rgb_values to populate term_color_code_rgb_values with RGB tuples (we set the empty value (-1, -1, -1) if the exact RGB value for the color code cannot be computed). From personal experimentation, it appears that the first 16 "Standard" and "High-intensity" color codes map to different RGB values depending on the terminal, whereas the remaining 216 color codes and the last 24 grayscale color codes stay the same, so we will only compute RGB values in the latter 2 cases.
The 216 color codes adhere to the formula:
color_code = (r_0to5*6^2 + g_0to5*6^1 + b_0to5*6^0) + 16
r/g/b_0to5 can each have one of 6 values from 0 to 5: r/g/b_0to5 in {0, 1, 2, 3, 4, 5} == True. There are 6 possible values for each variable because the total number of possible combinations equals 216: 6*6*6 = 6^3 = 216. To convert the color codes in this 216 range to RGB we must:
- Subtract the offset of
16 - Extract
r/g/b_0to5 - Map the
r/g/b_0to5values that vary between0and5to values between0and255(inclusive)
Naively, you might think that the mapping in step 3 uses even spacing. E.g.:
0 -> 0*255/5
1 -> 1*255/5
2 -> 2*255/5
3 -> 3*255/5
4 -> 4*255/5
5 -> 5*255/5
In reality, the interval between the first and second value is 95 and the remaining values are spaced by an interval of 40 (as verified by experimentation):
0 -> 0
1 -> 95
2 -> 95 + 40
3 -> 95 + 80
4 -> 95 + 120
5 -> 95 + 160
With the mapping, we can now implement the code to populate term_color_code_rgb_values for the 216 codes:
if 16 <= color_code < 256-24:
color_code -= 16
color_code, b_0to5 = divmod(color_code, 6)
color_code, g_0to5 = divmod(color_code, 6)
color_code, r_0to5 = divmod(color_code, 6)
map_0to5_to_0to255 = [0, 0+95,
40+95,
80+95,
120+95,
160+95]
term_color_code_rgb_values.append((
map_0to5_to_0to255[r_0to5],
map_0to5_to_0to255[g_0to5],
map_0to5_to_0to255[b_0to5]))
The last 24 grayscale color codes are easier compute:
- Subtract the offset of
256-24for thecode_code - Compute the the grayscale value
10*(color_code+1) - 2
if color_code >= 256-24:
color_code -= 256-24
grayscale_0to255 = 10*(color_code+1) - 2
term_color_code_rgb_values.append(
(grayscale_0to255, grayscale_0to255, grayscale_0to255))
The final implementation of populate_term_color_code_rgb_values is as follows:
term_color_code_rgb_values = []
def populate_term_color_code_rgb_values():
for color_code in range(256):
if 16 <= color_code < 256-24:
color_code -= 16
color_code, b_0to5 = divmod(color_code, 6)
color_code, g_0to5 = divmod(color_code, 6)
color_code, r_0to5 = divmod(color_code, 6)
map_0to5_to_0to255 = [0, 0+95,
40+95,
80+95,
120+95,
160+95]
term_color_code_rgb_values.append((
map_0to5_to_0to255[r_0to5],
map_0to5_to_0to255[g_0to5],
map_0to5_to_0to255[b_0to5]))
elif color_code >= 256-24:
color_code -= 256-24
grayscale_0to255 = 10*(color_code+1) - 2
term_color_code_rgb_values.append(
(grayscale_0to255, grayscale_0to255, grayscale_0to255))
else:
term_color_code_rgb_values.append((-1, -1, -1))
Very old/minimal terminals
Very old or minimal terminals may only support the first 8 or 16 color codes. As mentioned, the exact RGB values for these color codes are implementation-dependent. That being said, they follow a consistent scheme where:
- The most significant bit is set if the color is "high-intensity"
- The least significant bit is set if the color has a red component
- The 2nd least significant bit is set if the color has a green component
- The 3rd least significant bit is set if the color has a blue component
Code Binary is_high_intensity has_r has_g has_ b
0 0b0000 0 0 0 0 Standard Black
1 0b0001 0 1 0 0 Standard Red
2 0b0010 0 0 1 0 Standard Green
3 0b0011 0 1 1 0 Standard Yellow
4 0b0100 0 0 0 1 Standard Blue
5 0b0101 0 1 0 1 Standard Magenta
6 0b0110 0 0 1 1 Standard Cyan
7 0b0111 0 1 1 1 Standard White
8 0b1000 1 0 0 0 High-Intensity Black
9 0b1001 1 1 0 0 High-Intensity Red
10 0b1010 1 0 1 0 High-Intensity Green
11 0b1011 1 1 1 0 High-Intensity Yellow
12 0b1100 1 0 0 1 High-Intensity Blue
13 0b1101 1 1 0 1 High-Intensity Magenta
14 0b1110 1 0 1 1 High-Intensity Cyan
15 0b1111 1 1 1 1 High-Intensity White
With a bit of bit-shifting we can easily extract this information:
for color_code in range(16):
is_high_intensity = color_code>>3
has_r = color_code &1
has_g = (color_code>>1)&1
has_b = (color_code>>2)&1
The calculation for the exact RGB value of the color code is dependent on the terminal. On virtual consoles I observed that the CGA color palette is used. The following code works for CGA:
rgb = (has_r*0xAA + is_high_intensity*0x55,
has_g*0xAA + is_high_intensity*0x55,
has_b*0xAA + is_high_intensity*0x55)
For Xterm the following works:
factor = 0xFF if is_high_intensity else 0xCD
rgb = (has_r*factor, has_g*factor, has_b*factor)
The Gnome Terminal does not seem to follow a formula and seems to have uniquely chosen values for each of the 16 color codes.
Depending on your use cases you could change populate_term_color_code_rgb_values to use one of these computations or a best-guess average.
Detecting terminal capabilities
There are many ways to obtain information about the color model that a terminal supports. The Arch Linux wiki page has a lot of information on the topic. An autodetection implementation could do something like (pseudocode):
if 24-bit color escape sequence support
use "\033[38;2;{r};{g};{b}m" and "\033[48;2;{r};{g};{b}m"
else if $( tput --colors ) == 256
use 216 and grayscale color code to RGB mapping
else
use first 16 color codes to CGA RGB mapping
Many commands such as ls and grep use the flag --colors=auto|always|never. It's probably worth checking out how they implement auto though the coreutils codebase is not the most readable IMO.
For an initial implementation of a command line application I would just turn colors off by default and let the user optional specify which capabilities their terminal supports. Autodetection is rabbit hole that you can go down once you have something working.
Notes
- I wrote this answer because I wanted to document the information that I found somewhere
calc_color_distcould/should use a better color distance metric. See the Color Difference Wikipedia article