-
-
Save MicahElliott/719710 to your computer and use it in GitHub Desktop.
#! /usr/bin/env python | |
""" 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. | |
Resources: | |
* http://en.wikipedia.org/wiki/8-bit_color | |
* http://en.wikipedia.org/wiki/ANSI_escape_code | |
* /usr/share/X11/rgb.txt | |
I'm not sure where this script was inspired from. I think I must have | |
written it from scratch, though it's been several years now. | |
""" | |
__author__ = 'Micah Elliott http://MicahElliott.com' | |
__version__ = '0.1' | |
__copyright__ = 'Copyright (C) 2011 Micah Elliott. All rights reserved.' | |
__license__ = 'WTFPL http://sam.zoy.org/wtfpl/' | |
#--------------------------------------------------------------------- | |
import sys, re | |
CLUT = [ # color look-up table | |
# 8-bit, RGB hex | |
# Primary 3-bit (8 colors). Unique representation! | |
('00', '000000'), | |
('01', '800000'), | |
('02', '008000'), | |
('03', '808000'), | |
('04', '000080'), | |
('05', '800080'), | |
('06', '008080'), | |
('07', 'c0c0c0'), | |
# Equivalent "bright" versions of original 8 colors. | |
('08', '808080'), | |
('09', 'ff0000'), | |
('10', '00ff00'), | |
('11', 'ffff00'), | |
('12', '0000ff'), | |
('13', 'ff00ff'), | |
('14', '00ffff'), | |
('15', 'ffffff'), | |
# 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 _str2hex(hexstr): | |
return int(hexstr, 16) | |
def _strip_hash(rgb): | |
# Strip leading `#` if exists. | |
if rgb.startswith('#'): | |
rgb = rgb.lstrip('#') | |
return rgb | |
def _create_dicts(): | |
short2rgb_dict = dict(CLUT) | |
rgb2short_dict = {} | |
for k, v in short2rgb_dict.items(): | |
rgb2short_dict[v] = k | |
return rgb2short_dict, short2rgb_dict | |
def short2rgb(short): | |
return SHORT2RGB_DICT[short] | |
def print_all(): | |
""" 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." | |
def rgb2short(rgb): | |
""" Find the closest xterm-256 approximation to the given RGB value. | |
@param rgb: Hex code representing an RGB value, eg, 'abcdef' | |
@returns: String between 0 and 255, compatible with xterm. | |
>>> rgb2short('123456') | |
('23', '005f5f') | |
>>> rgb2short('ffffff') | |
('231', 'ffffff') | |
>>> rgb2short('0DADD6') # vimeo logo | |
('38', '00afd7') | |
""" | |
rgb = _strip_hash(rgb) | |
incs = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff) | |
# Break 6-char RGB code into 3 integer vals. | |
parts = [ int(h, 16) for h in re.split(r'(..)(..)(..)', rgb)[1:4] ] | |
res = [] | |
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) | |
if s1 < b1: closest = s | |
else: closest = b | |
res.append(closest) | |
break | |
i += 1 | |
#print '***', res | |
res = ''.join([ ('%02.x' % i) for i in res ]) | |
equiv = RGB2SHORT_DICT[ res ] | |
#print '***', res, equiv | |
return equiv, res | |
RGB2SHORT_DICT, SHORT2RGB_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: | |
short, rgb = rgb2short(arg) | |
sys.stdout.write('RGB %s -> xterm color approx \033[38;5;%sm%s (%s)' % (arg, short, short, rgb)) | |
sys.stdout.write("\033[0m\n") |
262626 returns 0 instead of 235
I admit to being a bit bewildered by the nested loops and complexity in your rgb2short function.
My own attempt (which I wrote before seeing yours) is somewhat briefer:
# Default color levels for the color cube
cubelevels = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
# Generate a list of midpoints of the above list
snaps = [(x+y)/2 for x, y in zip(cubelevels, [0]+cubelevels)[1:]]
def rgb2short(r, g, b):
""" Converts RGB values to the nearest equivalent xterm-256 color.
"""
# Using list of snap points, convert RGB value to cube indexes
r, g, b = map(lambda x: len(tuple(s for s in snaps if s<x)), (r, g, b))
# Simple colorcube transform
return r*36 + g*6 + b + 16
A recommendation for anyone wanting to do this "perfectly": It is preferable to calculate distances in YUV or CIE94 than in RGB.
Distances in the YUV and CIE94 colorspaces more closely match distances in human perception. So using one of those should output colors which appear (to our eyes) to be more similar to the input colors.
YUV is often used for image and video encoding. CIE94 is the more accurate, but YUV is faster/easier to calculate and still a good approximation.
Having said that, when quantizing as coarsely as we are here, doing this will only occasionally produce different results.
I love it, specially the approx. part. cheers mate, you're awesome
Great script thanks for sharing!
See this doc for an explanation of how to convert to/from SGR codes to RGB and how to manipulate RGB and HSV in Bash.
https://github.com/chadj2/bash-ui/blob/master/COLORS.md#xterm-colorspaces
Thanks for writing this, I've included it in a project of mine as a utility script. I've credited you in the README and added an annotation to the script itself. If you'd like me to add credit in a different way or to not include it in my package please let me know.
Thanks for writing this :-)
@TerrorBite, I made your code into a lib + stand alone hybrid. Also, I had to change your snaps
definition to move the [1:]
outside of the list comprehension. I don't know if that was an actual bug in your code or something wrong with how I used it in Python3. Thanks for your work.
@chadj2, that is an awesome write up. I'm inspired to try to convert the code I have below into pure bash.
#!/usr/bin/env python3
# Default color levels for the color cube
cubelevels = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
# Generate a list of midpoints of the above list
snaps = [(x+y)/2 for x, y in zip(cubelevels, [0]+cubelevels)][1:]
def rgb2short(r, g, b):
""" Converts RGB values to the nearest equivalent xterm-256 color."""
# Using list of snap points, convert RGB value to cube indexes
r, g, b = map(lambda x: len(tuple(s for s in snaps if s < x)), (r, g, b))
# Simple colorcube transform
return r*36 + g*6 + b + 16
def split_into(s, l):
return [int(s[1][y-l:y], 16) for y in range(l, len(s[1]) + l, l)]
def main():
""" Pass either 1 hex rgb string or split into 3."""
import sys
if len(sys.argv) < 3:
args = split_into(sys.argv, 2)
else:
args = sys.argv
print(rgb2short(args[0], args[1], args[2]))
if __name__ == '__main__':
main()
@RichardBronosky I am glad the write up helped. I am seeing you have hundreds of console colors. Consider using a few base colors and allowing the user to adjust them with HSV (explained in the write up).
Harshly saturated colors can be overwhelming for the eyes and washed out or pastel colors can be preferable (e.g. https://ethanschoonover.com/solarized/). The Saturation level allows for adjusting this. Hue would be the base color.
Exactly what I wanted!
Does anyone know how to do the same for 16 color codes?
@TerrorBite, I made your code into a lib + stand alone hybrid. Also, I had to change your
snaps
definition to move the[1:]
outside of the list comprehension. I don't know if that was an actual bug in your code or something wrong with how I used it in Python3. Thanks for your work.
Thanks! So in Python 2, the map()
function returns a list, which you can slice. In Python 3, it returns a generator, thus the slice has to be done on the list comprehension after the whole thing is generated.
Thanks for the info! I wonder if exist a pure mathematical formula to get the conversion RGB to the nearest ANSI 256 color code. I would to like implement under C.
Here's a web UI I made to do this conversion
https://hm66hd.csb.app/
Here's a web UI I made to do this conversion https://hm66hd.csb.app/
Thanks for that, quite helpful!
buggy script, the 256-color conversion is all wrong
Here's a web UI I made to do this conversion https://hm66hd.csb.app/
this is correct converter, but cannot found source code
Bug: #242424 -> 16, should be 235