blob: 5ced0e71b543a11b67bc878fcb6462b56ee996db [file] [log] [blame]
Barry Warsaw2406b1d1998-01-31 00:29:41 +00001"""Color Database.
2
Barry Warsawa9053f61998-10-02 16:01:42 +00003This file contains one class, called ColorDB, and several utility functions.
4The class must be instantiated by the get_colordb() function in this file,
5passing it a filename to read a database out of.
6
7The get_colordb() function will try to examine the file to figure out what the
8format of the file is. If it can't figure out the file format, or it has
9trouble reading the file, None is returned. You can pass get_colordb() an
10optional filetype argument.
Barry Warsaw2406b1d1998-01-31 00:29:41 +000011
12Supporte file types are:
13
14 X_RGB_TXT -- X Consortium rgb.txt format files. Three columns of numbers
15 from 0 .. 255 separated by whitespace. Arbitrary trailing
16 columns used as the color name.
Barry Warsawa9053f61998-10-02 16:01:42 +000017
18The utility functions are useful for converting between the various expected
19color formats, and for calculating other color values.
20
Barry Warsaw2406b1d1998-01-31 00:29:41 +000021"""
22
23import sys
24import re
Barry Warsawf4562a71998-02-11 17:19:23 +000025from types import *
Barry Warsaw2406b1d1998-01-31 00:29:41 +000026
Barry Warsaw9f4d73a1998-01-31 23:38:48 +000027class BadColor(Exception):
28 pass
29
Barry Warsawf4562a71998-02-11 17:19:23 +000030DEFAULT_DB = None
Barry Warsaw6c50ae02001-07-10 21:38:47 +000031SPACE = ' '
32COMMASPACE = ', '
Barry Warsawf4562a71998-02-11 17:19:23 +000033
Barry Warsawa5a018f1998-09-25 22:51:36 +000034
Barry Warsaw2406b1d1998-01-31 00:29:41 +000035
36# generic class
37class ColorDB:
Barry Warsaw0604d721999-04-26 23:17:16 +000038 def __init__(self, fp):
39 lineno = 2
40 self.__name = fp.name
Barry Warsaw5c458052002-10-21 14:25:24 +000041 # Maintain several dictionaries for indexing into the color database.
42 # Note that while Tk supports RGB intensities of 4, 8, 12, or 16 bits,
43 # for now we only support 8 bit intensities. At least on OpenWindows,
44 # all intensities in the /usr/openwin/lib/rgb.txt file are 8-bit
45 #
46 # key is (red, green, blue) tuple, value is (name, [aliases])
47 self.__byrgb = {}
48 # key is name, value is (red, green, blue)
49 self.__byname = {}
Barry Warsaw8be25941998-10-02 14:43:30 +000050 # all unique names (non-aliases). built-on demand
51 self.__allnames = None
Georg Brandl86def6c2008-01-21 20:36:10 +000052 for line in fp:
Barry Warsaw5c458052002-10-21 14:25:24 +000053 # get this compiled regular expression from derived class
54 mo = self._re.match(line)
55 if not mo:
Collin Winter6afaeb72007-08-03 17:06:41 +000056 print('Error in', fp.name, ' line', lineno, file=sys.stderr)
Barry Warsaw5c458052002-10-21 14:25:24 +000057 lineno += 1
58 continue
59 # extract the red, green, blue, and name
Barry Warsaw0604d721999-04-26 23:17:16 +000060 red, green, blue = self._extractrgb(mo)
61 name = self._extractname(mo)
Barry Warsaw5c458052002-10-21 14:25:24 +000062 keyname = name.lower()
63 # BAW: for now the `name' is just the first named color with the
64 # rgb values we find. Later, we might want to make the two word
65 # version the `name', or the CapitalizedVersion, etc.
66 key = (red, green, blue)
67 foundname, aliases = self.__byrgb.get(key, (name, []))
Georg Brandlbf82e372008-05-16 17:02:34 +000068 if foundname != name and foundname not in aliases:
Barry Warsaw5c458052002-10-21 14:25:24 +000069 aliases.append(name)
70 self.__byrgb[key] = (foundname, aliases)
71 # add to byname lookup
72 self.__byname[keyname] = key
73 lineno = lineno + 1
Barry Warsaw2406b1d1998-01-31 00:29:41 +000074
Barry Warsaw0604d721999-04-26 23:17:16 +000075 # override in derived classes
76 def _extractrgb(self, mo):
Barry Warsaw6c50ae02001-07-10 21:38:47 +000077 return [int(x) for x in mo.group('red', 'green', 'blue')]
Barry Warsaw0604d721999-04-26 23:17:16 +000078
79 def _extractname(self, mo):
80 return mo.group('name')
81
82 def filename(self):
83 return self.__name
84
Barry Warsaweb9b8af1998-02-11 18:55:37 +000085 def find_byrgb(self, rgbtuple):
Barry Warsaw0604d721999-04-26 23:17:16 +000086 """Return name for rgbtuple"""
Barry Warsaw5c458052002-10-21 14:25:24 +000087 try:
88 return self.__byrgb[rgbtuple]
89 except KeyError:
90 raise BadColor(rgbtuple)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000091
92 def find_byname(self, name):
Barry Warsaw0604d721999-04-26 23:17:16 +000093 """Return (red, green, blue) for name"""
Barry Warsaw5c458052002-10-21 14:25:24 +000094 name = name.lower()
95 try:
96 return self.__byname[name]
97 except KeyError:
98 raise BadColor(name)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000099
Barry Warsaw7a134181998-09-29 20:03:15 +0000100 def nearest(self, red, green, blue):
Barry Warsaw0604d721999-04-26 23:17:16 +0000101 """Return the name of color nearest (red, green, blue)"""
Barry Warsaw5c458052002-10-21 14:25:24 +0000102 # BAW: should we use Voronoi diagrams, Delaunay triangulation, or
103 # octree for speeding up the locating of nearest point? Exhaustive
104 # search is inefficient, but seems fast enough.
105 nearest = -1
106 nearest_name = ''
107 for name, aliases in self.__byrgb.values():
108 r, g, b = self.__byname[name.lower()]
109 rdelta = red - r
110 gdelta = green - g
111 bdelta = blue - b
112 distance = rdelta * rdelta + gdelta * gdelta + bdelta * bdelta
113 if nearest == -1 or distance < nearest:
114 nearest = distance
115 nearest_name = name
116 return nearest_name
Barry Warsaw8be25941998-10-02 14:43:30 +0000117
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000118 def unique_names(self):
Barry Warsaw8be25941998-10-02 14:43:30 +0000119 # sorted
120 if not self.__allnames:
121 self.__allnames = []
122 for name, aliases in self.__byrgb.values():
123 self.__allnames.append(name)
Georg Brandlbf82e372008-05-16 17:02:34 +0000124 self.__allnames.sort(key=str.lower)
Barry Warsaw8be25941998-10-02 14:43:30 +0000125 return self.__allnames
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000126
127 def aliases_of(self, red, green, blue):
128 try:
129 name, aliases = self.__byrgb[(red, green, blue)]
130 except KeyError:
131 raise BadColor((red, green, blue))
132 return [name] + aliases
Barry Warsaw5c458052002-10-21 14:25:24 +0000133
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000134
135class RGBColorDB(ColorDB):
136 _re = re.compile(
Barry Warsaw0604d721999-04-26 23:17:16 +0000137 '\s*(?P<red>\d+)\s+(?P<green>\d+)\s+(?P<blue>\d+)\s+(?P<name>.*)')
138
139
140class HTML40DB(ColorDB):
141 _re = re.compile('(?P<name>\S+)\s+(?P<hexrgb>#[0-9a-fA-F]{6})')
142
143 def _extractrgb(self, mo):
144 return rrggbb_to_triplet(mo.group('hexrgb'))
145
146class LightlinkDB(HTML40DB):
147 _re = re.compile('(?P<name>(.+))\s+(?P<hexrgb>#[0-9a-fA-F]{6})')
148
149 def _extractname(self, mo):
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000150 return mo.group('name').strip()
Barry Warsaw0604d721999-04-26 23:17:16 +0000151
152class WebsafeDB(ColorDB):
153 _re = re.compile('(?P<hexrgb>#[0-9a-fA-F]{6})')
154
155 def _extractrgb(self, mo):
156 return rrggbb_to_triplet(mo.group('hexrgb'))
157
158 def _extractname(self, mo):
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000159 return mo.group('hexrgb').upper()
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000160
161
162
163# format is a tuple (RE, SCANLINES, CLASS) where RE is a compiled regular
164# expression, SCANLINES is the number of header lines to scan, and CLASS is
165# the class to instantiate if a match is found
166
Barry Warsaw0604d721999-04-26 23:17:16 +0000167FILETYPES = [
Barry Warsaw5c458052002-10-21 14:25:24 +0000168 (re.compile('Xorg'), RGBColorDB),
Barry Warsaw0604d721999-04-26 23:17:16 +0000169 (re.compile('XConsortium'), RGBColorDB),
170 (re.compile('HTML'), HTML40DB),
171 (re.compile('lightlink'), LightlinkDB),
172 (re.compile('Websafe'), WebsafeDB),
173 ]
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000174
Barry Warsaw0604d721999-04-26 23:17:16 +0000175def get_colordb(file, filetype=None):
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000176 colordb = None
Barry Warsaw0604d721999-04-26 23:17:16 +0000177 fp = open(file)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000178 try:
Barry Warsaw0604d721999-04-26 23:17:16 +0000179 line = fp.readline()
180 if not line:
181 return None
182 # try to determine the type of RGB file it is
183 if filetype is None:
184 filetypes = FILETYPES
185 else:
186 filetypes = [filetype]
187 for typere, class_ in filetypes:
188 mo = typere.search(line)
189 if mo:
190 break
191 else:
192 # no matching type
193 return None
194 # we know the type and the class to grok the type, so suck it in
195 colordb = class_(fp)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000196 finally:
Barry Warsaw0604d721999-04-26 23:17:16 +0000197 fp.close()
Barry Warsawf4562a71998-02-11 17:19:23 +0000198 # save a global copy
199 global DEFAULT_DB
200 DEFAULT_DB = colordb
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000201 return colordb
202
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000203
204
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000205_namedict = {}
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000206
207def rrggbb_to_triplet(color):
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000208 """Converts a #rrggbb color to the tuple (red, green, blue)."""
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000209 rgbtuple = _namedict.get(color)
210 if rgbtuple is None:
Georg Brandlbf82e372008-05-16 17:02:34 +0000211 if color[0] != '#':
Barry Warsawb7b1cf01998-10-06 18:10:59 +0000212 raise BadColor(color)
Barry Warsaw5c458052002-10-21 14:25:24 +0000213 red = color[1:3]
214 green = color[3:5]
215 blue = color[5:7]
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000216 rgbtuple = int(red, 16), int(green, 16), int(blue, 16)
Barry Warsaw5c458052002-10-21 14:25:24 +0000217 _namedict[color] = rgbtuple
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000218 return rgbtuple
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000219
Barry Warsawf4562a71998-02-11 17:19:23 +0000220
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000221_tripdict = {}
Barry Warsawf4562a71998-02-11 17:19:23 +0000222def triplet_to_rrggbb(rgbtuple):
223 """Converts a (red, green, blue) tuple to #rrggbb."""
Barry Warsaw0604d721999-04-26 23:17:16 +0000224 global _tripdict
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000225 hexname = _tripdict.get(rgbtuple)
226 if hexname is None:
Barry Warsaw5c458052002-10-21 14:25:24 +0000227 hexname = '#%02x%02x%02x' % rgbtuple
228 _tripdict[rgbtuple] = hexname
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000229 return hexname
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000230
231
Barry Warsawa5a018f1998-09-25 22:51:36 +0000232def triplet_to_fractional_rgb(rgbtuple):
Serhiy Storchakaa60c2fe2015-03-12 21:56:08 +0200233 return [x / 256 for x in rgbtuple]
Barry Warsaw2662e151998-02-13 21:27:56 +0000234
235
Barry Warsaw0e3e6991998-09-28 23:39:18 +0000236def triplet_to_brightness(rgbtuple):
237 # return the brightness (grey level) along the scale 0.0==black to
238 # 1.0==white
239 r = 0.299
240 g = 0.587
241 b = 0.114
242 return r*rgbtuple[0] + g*rgbtuple[1] + b*rgbtuple[2]
243
244
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000245
246if __name__ == '__main__':
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000247 colordb = get_colordb('/usr/openwin/lib/rgb.txt')
248 if not colordb:
Collin Winter6afaeb72007-08-03 17:06:41 +0000249 print('No parseable color database found')
Barry Warsaw5c458052002-10-21 14:25:24 +0000250 sys.exit(1)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000251 # on my system, this color matches exactly
252 target = 'navy'
Barry Warsawa5a018f1998-09-25 22:51:36 +0000253 red, green, blue = rgbtuple = colordb.find_byname(target)
Collin Winter6afaeb72007-08-03 17:06:41 +0000254 print(target, ':', red, green, blue, triplet_to_rrggbb(rgbtuple))
Barry Warsawa5a018f1998-09-25 22:51:36 +0000255 name, aliases = colordb.find_byrgb(rgbtuple)
Collin Winter6afaeb72007-08-03 17:06:41 +0000256 print('name:', name, 'aliases:', COMMASPACE.join(aliases))
Barry Warsaw5c458052002-10-21 14:25:24 +0000257 r, g, b = (1, 1, 128) # nearest to navy
258 r, g, b = (145, 238, 144) # nearest to lightgreen
259 r, g, b = (255, 251, 250) # snow
Collin Winter6afaeb72007-08-03 17:06:41 +0000260 print('finding nearest to', target, '...')
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000261 import time
262 t0 = time.time()
Barry Warsaw840a84d1998-10-15 02:18:08 +0000263 nearest = colordb.nearest(r, g, b)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000264 t1 = time.time()
Collin Winter6afaeb72007-08-03 17:06:41 +0000265 print('found nearest color', nearest, 'in', t1-t0, 'seconds')
Barry Warsaw840a84d1998-10-15 02:18:08 +0000266 # dump the database
267 for n in colordb.unique_names():
268 r, g, b = colordb.find_byname(n)
269 aliases = colordb.aliases_of(r, g, b)
Collin Winter6afaeb72007-08-03 17:06:41 +0000270 print('%20s: (%3d/%3d/%3d) == %s' % (n, r, g, b,
271 SPACE.join(aliases[1:])))