blob: fcf1b87cd27c60cd114707c47fe6d8c2fb8b6499 [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 Warsaw8d3e5ee1998-02-18 00:02:26 +000026import operator
Barry Warsaw2406b1d1998-01-31 00:29:41 +000027
Barry Warsaw9f4d73a1998-01-31 23:38:48 +000028class BadColor(Exception):
29 pass
30
Barry Warsawf4562a71998-02-11 17:19:23 +000031DEFAULT_DB = None
Barry Warsaw6c50ae02001-07-10 21:38:47 +000032SPACE = ' '
33COMMASPACE = ', '
Barry Warsawf4562a71998-02-11 17:19:23 +000034
Barry Warsawa5a018f1998-09-25 22:51:36 +000035
Barry Warsaw2406b1d1998-01-31 00:29:41 +000036
37# generic class
38class ColorDB:
Barry Warsaw0604d721999-04-26 23:17:16 +000039 def __init__(self, fp):
40 lineno = 2
41 self.__name = fp.name
Barry Warsaw5c458052002-10-21 14:25:24 +000042 # Maintain several dictionaries for indexing into the color database.
43 # Note that while Tk supports RGB intensities of 4, 8, 12, or 16 bits,
44 # for now we only support 8 bit intensities. At least on OpenWindows,
45 # all intensities in the /usr/openwin/lib/rgb.txt file are 8-bit
46 #
47 # key is (red, green, blue) tuple, value is (name, [aliases])
48 self.__byrgb = {}
49 # key is name, value is (red, green, blue)
50 self.__byname = {}
Barry Warsaw8be25941998-10-02 14:43:30 +000051 # all unique names (non-aliases). built-on demand
52 self.__allnames = None
Georg Brandl86def6c2008-01-21 20:36:10 +000053 for line in fp:
Barry Warsaw5c458052002-10-21 14:25:24 +000054 # get this compiled regular expression from derived class
55 mo = self._re.match(line)
56 if not mo:
Collin Winter6afaeb72007-08-03 17:06:41 +000057 print('Error in', fp.name, ' line', lineno, file=sys.stderr)
Barry Warsaw5c458052002-10-21 14:25:24 +000058 lineno += 1
59 continue
60 # extract the red, green, blue, and name
Barry Warsaw0604d721999-04-26 23:17:16 +000061 red, green, blue = self._extractrgb(mo)
62 name = self._extractname(mo)
Barry Warsaw5c458052002-10-21 14:25:24 +000063 keyname = name.lower()
64 # BAW: for now the `name' is just the first named color with the
65 # rgb values we find. Later, we might want to make the two word
66 # version the `name', or the CapitalizedVersion, etc.
67 key = (red, green, blue)
68 foundname, aliases = self.__byrgb.get(key, (name, []))
Georg Brandlbf82e372008-05-16 17:02:34 +000069 if foundname != name and foundname not in aliases:
Barry Warsaw5c458052002-10-21 14:25:24 +000070 aliases.append(name)
71 self.__byrgb[key] = (foundname, aliases)
72 # add to byname lookup
73 self.__byname[keyname] = key
74 lineno = lineno + 1
Barry Warsaw2406b1d1998-01-31 00:29:41 +000075
Barry Warsaw0604d721999-04-26 23:17:16 +000076 # override in derived classes
77 def _extractrgb(self, mo):
Barry Warsaw6c50ae02001-07-10 21:38:47 +000078 return [int(x) for x in mo.group('red', 'green', 'blue')]
Barry Warsaw0604d721999-04-26 23:17:16 +000079
80 def _extractname(self, mo):
81 return mo.group('name')
82
83 def filename(self):
84 return self.__name
85
Barry Warsaweb9b8af1998-02-11 18:55:37 +000086 def find_byrgb(self, rgbtuple):
Barry Warsaw0604d721999-04-26 23:17:16 +000087 """Return name for rgbtuple"""
Barry Warsaw5c458052002-10-21 14:25:24 +000088 try:
89 return self.__byrgb[rgbtuple]
90 except KeyError:
91 raise BadColor(rgbtuple)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000092
93 def find_byname(self, name):
Barry Warsaw0604d721999-04-26 23:17:16 +000094 """Return (red, green, blue) for name"""
Barry Warsaw5c458052002-10-21 14:25:24 +000095 name = name.lower()
96 try:
97 return self.__byname[name]
98 except KeyError:
99 raise BadColor(name)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000100
Barry Warsaw7a134181998-09-29 20:03:15 +0000101 def nearest(self, red, green, blue):
Barry Warsaw0604d721999-04-26 23:17:16 +0000102 """Return the name of color nearest (red, green, blue)"""
Barry Warsaw5c458052002-10-21 14:25:24 +0000103 # BAW: should we use Voronoi diagrams, Delaunay triangulation, or
104 # octree for speeding up the locating of nearest point? Exhaustive
105 # search is inefficient, but seems fast enough.
106 nearest = -1
107 nearest_name = ''
108 for name, aliases in self.__byrgb.values():
109 r, g, b = self.__byname[name.lower()]
110 rdelta = red - r
111 gdelta = green - g
112 bdelta = blue - b
113 distance = rdelta * rdelta + gdelta * gdelta + bdelta * bdelta
114 if nearest == -1 or distance < nearest:
115 nearest = distance
116 nearest_name = name
117 return nearest_name
Barry Warsaw8be25941998-10-02 14:43:30 +0000118
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000119 def unique_names(self):
Barry Warsaw8be25941998-10-02 14:43:30 +0000120 # sorted
121 if not self.__allnames:
122 self.__allnames = []
123 for name, aliases in self.__byrgb.values():
124 self.__allnames.append(name)
Georg Brandlbf82e372008-05-16 17:02:34 +0000125 self.__allnames.sort(key=str.lower)
Barry Warsaw8be25941998-10-02 14:43:30 +0000126 return self.__allnames
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000127
128 def aliases_of(self, red, green, blue):
129 try:
130 name, aliases = self.__byrgb[(red, green, blue)]
131 except KeyError:
132 raise BadColor((red, green, blue))
133 return [name] + aliases
Barry Warsaw5c458052002-10-21 14:25:24 +0000134
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000135
136class RGBColorDB(ColorDB):
137 _re = re.compile(
Barry Warsaw0604d721999-04-26 23:17:16 +0000138 '\s*(?P<red>\d+)\s+(?P<green>\d+)\s+(?P<blue>\d+)\s+(?P<name>.*)')
139
140
141class HTML40DB(ColorDB):
142 _re = re.compile('(?P<name>\S+)\s+(?P<hexrgb>#[0-9a-fA-F]{6})')
143
144 def _extractrgb(self, mo):
145 return rrggbb_to_triplet(mo.group('hexrgb'))
146
147class LightlinkDB(HTML40DB):
148 _re = re.compile('(?P<name>(.+))\s+(?P<hexrgb>#[0-9a-fA-F]{6})')
149
150 def _extractname(self, mo):
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000151 return mo.group('name').strip()
Barry Warsaw0604d721999-04-26 23:17:16 +0000152
153class WebsafeDB(ColorDB):
154 _re = re.compile('(?P<hexrgb>#[0-9a-fA-F]{6})')
155
156 def _extractrgb(self, mo):
157 return rrggbb_to_triplet(mo.group('hexrgb'))
158
159 def _extractname(self, mo):
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000160 return mo.group('hexrgb').upper()
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000161
162
163
164# format is a tuple (RE, SCANLINES, CLASS) where RE is a compiled regular
165# expression, SCANLINES is the number of header lines to scan, and CLASS is
166# the class to instantiate if a match is found
167
Barry Warsaw0604d721999-04-26 23:17:16 +0000168FILETYPES = [
Barry Warsaw5c458052002-10-21 14:25:24 +0000169 (re.compile('Xorg'), RGBColorDB),
Barry Warsaw0604d721999-04-26 23:17:16 +0000170 (re.compile('XConsortium'), RGBColorDB),
171 (re.compile('HTML'), HTML40DB),
172 (re.compile('lightlink'), LightlinkDB),
173 (re.compile('Websafe'), WebsafeDB),
174 ]
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000175
Barry Warsaw0604d721999-04-26 23:17:16 +0000176def get_colordb(file, filetype=None):
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000177 colordb = None
Barry Warsaw0604d721999-04-26 23:17:16 +0000178 fp = open(file)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000179 try:
Barry Warsaw0604d721999-04-26 23:17:16 +0000180 line = fp.readline()
181 if not line:
182 return None
183 # try to determine the type of RGB file it is
184 if filetype is None:
185 filetypes = FILETYPES
186 else:
187 filetypes = [filetype]
188 for typere, class_ in filetypes:
189 mo = typere.search(line)
190 if mo:
191 break
192 else:
193 # no matching type
194 return None
195 # we know the type and the class to grok the type, so suck it in
196 colordb = class_(fp)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000197 finally:
Barry Warsaw0604d721999-04-26 23:17:16 +0000198 fp.close()
Barry Warsawf4562a71998-02-11 17:19:23 +0000199 # save a global copy
200 global DEFAULT_DB
201 DEFAULT_DB = colordb
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000202 return colordb
203
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000204
205
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000206_namedict = {}
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000207
208def rrggbb_to_triplet(color):
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000209 """Converts a #rrggbb color to the tuple (red, green, blue)."""
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000210 rgbtuple = _namedict.get(color)
211 if rgbtuple is None:
Georg Brandlbf82e372008-05-16 17:02:34 +0000212 if color[0] != '#':
Barry Warsawb7b1cf01998-10-06 18:10:59 +0000213 raise BadColor(color)
Barry Warsaw5c458052002-10-21 14:25:24 +0000214 red = color[1:3]
215 green = color[3:5]
216 blue = color[5:7]
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000217 rgbtuple = int(red, 16), int(green, 16), int(blue, 16)
Barry Warsaw5c458052002-10-21 14:25:24 +0000218 _namedict[color] = rgbtuple
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000219 return rgbtuple
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000220
Barry Warsawf4562a71998-02-11 17:19:23 +0000221
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000222_tripdict = {}
Barry Warsawf4562a71998-02-11 17:19:23 +0000223def triplet_to_rrggbb(rgbtuple):
224 """Converts a (red, green, blue) tuple to #rrggbb."""
Barry Warsaw0604d721999-04-26 23:17:16 +0000225 global _tripdict
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000226 hexname = _tripdict.get(rgbtuple)
227 if hexname is None:
Barry Warsaw5c458052002-10-21 14:25:24 +0000228 hexname = '#%02x%02x%02x' % rgbtuple
229 _tripdict[rgbtuple] = hexname
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000230 return hexname
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000231
232
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000233_maxtuple = (256.0,) * 3
Barry Warsawa5a018f1998-09-25 22:51:36 +0000234def triplet_to_fractional_rgb(rgbtuple):
Georg Brandlbf82e372008-05-16 17:02:34 +0000235 return list(map(operator.__div__, rgbtuple, _maxtuple))
Barry Warsaw2662e151998-02-13 21:27:56 +0000236
237
Barry Warsaw0e3e6991998-09-28 23:39:18 +0000238def triplet_to_brightness(rgbtuple):
239 # return the brightness (grey level) along the scale 0.0==black to
240 # 1.0==white
241 r = 0.299
242 g = 0.587
243 b = 0.114
244 return r*rgbtuple[0] + g*rgbtuple[1] + b*rgbtuple[2]
245
246
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000247
248if __name__ == '__main__':
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000249 colordb = get_colordb('/usr/openwin/lib/rgb.txt')
250 if not colordb:
Collin Winter6afaeb72007-08-03 17:06:41 +0000251 print('No parseable color database found')
Barry Warsaw5c458052002-10-21 14:25:24 +0000252 sys.exit(1)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000253 # on my system, this color matches exactly
254 target = 'navy'
Barry Warsawa5a018f1998-09-25 22:51:36 +0000255 red, green, blue = rgbtuple = colordb.find_byname(target)
Collin Winter6afaeb72007-08-03 17:06:41 +0000256 print(target, ':', red, green, blue, triplet_to_rrggbb(rgbtuple))
Barry Warsawa5a018f1998-09-25 22:51:36 +0000257 name, aliases = colordb.find_byrgb(rgbtuple)
Collin Winter6afaeb72007-08-03 17:06:41 +0000258 print('name:', name, 'aliases:', COMMASPACE.join(aliases))
Barry Warsaw5c458052002-10-21 14:25:24 +0000259 r, g, b = (1, 1, 128) # nearest to navy
260 r, g, b = (145, 238, 144) # nearest to lightgreen
261 r, g, b = (255, 251, 250) # snow
Collin Winter6afaeb72007-08-03 17:06:41 +0000262 print('finding nearest to', target, '...')
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000263 import time
264 t0 = time.time()
Barry Warsaw840a84d1998-10-15 02:18:08 +0000265 nearest = colordb.nearest(r, g, b)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000266 t1 = time.time()
Collin Winter6afaeb72007-08-03 17:06:41 +0000267 print('found nearest color', nearest, 'in', t1-t0, 'seconds')
Barry Warsaw840a84d1998-10-15 02:18:08 +0000268 # dump the database
269 for n in colordb.unique_names():
270 r, g, b = colordb.find_byname(n)
271 aliases = colordb.aliases_of(r, g, b)
Collin Winter6afaeb72007-08-03 17:06:41 +0000272 print('%20s: (%3d/%3d/%3d) == %s' % (n, r, g, b,
273 SPACE.join(aliases[1:])))