blob: 2e7a1b50e8320120538c802b873f73c707db96a8 [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
Barry Warsaw5c458052002-10-21 14:25:24 +000053 while 1:
54 line = fp.readline()
55 if not line:
56 break
57 # get this compiled regular expression from derived class
58 mo = self._re.match(line)
59 if not mo:
Collin Winter6afaeb72007-08-03 17:06:41 +000060 print('Error in', fp.name, ' line', lineno, file=sys.stderr)
Barry Warsaw5c458052002-10-21 14:25:24 +000061 lineno += 1
62 continue
63 # extract the red, green, blue, and name
Barry Warsaw0604d721999-04-26 23:17:16 +000064 red, green, blue = self._extractrgb(mo)
65 name = self._extractname(mo)
Barry Warsaw5c458052002-10-21 14:25:24 +000066 keyname = name.lower()
67 # BAW: for now the `name' is just the first named color with the
68 # rgb values we find. Later, we might want to make the two word
69 # version the `name', or the CapitalizedVersion, etc.
70 key = (red, green, blue)
71 foundname, aliases = self.__byrgb.get(key, (name, []))
72 if foundname <> name and foundname not in aliases:
73 aliases.append(name)
74 self.__byrgb[key] = (foundname, aliases)
75 # add to byname lookup
76 self.__byname[keyname] = key
77 lineno = lineno + 1
Barry Warsaw2406b1d1998-01-31 00:29:41 +000078
Barry Warsaw0604d721999-04-26 23:17:16 +000079 # override in derived classes
80 def _extractrgb(self, mo):
Barry Warsaw6c50ae02001-07-10 21:38:47 +000081 return [int(x) for x in mo.group('red', 'green', 'blue')]
Barry Warsaw0604d721999-04-26 23:17:16 +000082
83 def _extractname(self, mo):
84 return mo.group('name')
85
86 def filename(self):
87 return self.__name
88
Barry Warsaweb9b8af1998-02-11 18:55:37 +000089 def find_byrgb(self, rgbtuple):
Barry Warsaw0604d721999-04-26 23:17:16 +000090 """Return name for rgbtuple"""
Barry Warsaw5c458052002-10-21 14:25:24 +000091 try:
92 return self.__byrgb[rgbtuple]
93 except KeyError:
94 raise BadColor(rgbtuple)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000095
96 def find_byname(self, name):
Barry Warsaw0604d721999-04-26 23:17:16 +000097 """Return (red, green, blue) for name"""
Barry Warsaw5c458052002-10-21 14:25:24 +000098 name = name.lower()
99 try:
100 return self.__byname[name]
101 except KeyError:
102 raise BadColor(name)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000103
Barry Warsaw7a134181998-09-29 20:03:15 +0000104 def nearest(self, red, green, blue):
Barry Warsaw0604d721999-04-26 23:17:16 +0000105 """Return the name of color nearest (red, green, blue)"""
Barry Warsaw5c458052002-10-21 14:25:24 +0000106 # BAW: should we use Voronoi diagrams, Delaunay triangulation, or
107 # octree for speeding up the locating of nearest point? Exhaustive
108 # search is inefficient, but seems fast enough.
109 nearest = -1
110 nearest_name = ''
111 for name, aliases in self.__byrgb.values():
112 r, g, b = self.__byname[name.lower()]
113 rdelta = red - r
114 gdelta = green - g
115 bdelta = blue - b
116 distance = rdelta * rdelta + gdelta * gdelta + bdelta * bdelta
117 if nearest == -1 or distance < nearest:
118 nearest = distance
119 nearest_name = name
120 return nearest_name
Barry Warsaw8be25941998-10-02 14:43:30 +0000121
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000122 def unique_names(self):
Barry Warsaw8be25941998-10-02 14:43:30 +0000123 # sorted
124 if not self.__allnames:
125 self.__allnames = []
126 for name, aliases in self.__byrgb.values():
127 self.__allnames.append(name)
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000128 # sort irregardless of case
129 def nocase_cmp(n1, n2):
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000130 return cmp(n1.lower(), n2.lower())
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000131 self.__allnames.sort(nocase_cmp)
Barry Warsaw8be25941998-10-02 14:43:30 +0000132 return self.__allnames
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000133
134 def aliases_of(self, red, green, blue):
135 try:
136 name, aliases = self.__byrgb[(red, green, blue)]
137 except KeyError:
138 raise BadColor((red, green, blue))
139 return [name] + aliases
Barry Warsaw5c458052002-10-21 14:25:24 +0000140
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000141
142class RGBColorDB(ColorDB):
143 _re = re.compile(
Barry Warsaw0604d721999-04-26 23:17:16 +0000144 '\s*(?P<red>\d+)\s+(?P<green>\d+)\s+(?P<blue>\d+)\s+(?P<name>.*)')
145
146
147class HTML40DB(ColorDB):
148 _re = re.compile('(?P<name>\S+)\s+(?P<hexrgb>#[0-9a-fA-F]{6})')
149
150 def _extractrgb(self, mo):
151 return rrggbb_to_triplet(mo.group('hexrgb'))
152
153class LightlinkDB(HTML40DB):
154 _re = re.compile('(?P<name>(.+))\s+(?P<hexrgb>#[0-9a-fA-F]{6})')
155
156 def _extractname(self, mo):
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000157 return mo.group('name').strip()
Barry Warsaw0604d721999-04-26 23:17:16 +0000158
159class WebsafeDB(ColorDB):
160 _re = re.compile('(?P<hexrgb>#[0-9a-fA-F]{6})')
161
162 def _extractrgb(self, mo):
163 return rrggbb_to_triplet(mo.group('hexrgb'))
164
165 def _extractname(self, mo):
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000166 return mo.group('hexrgb').upper()
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000167
168
169
170# format is a tuple (RE, SCANLINES, CLASS) where RE is a compiled regular
171# expression, SCANLINES is the number of header lines to scan, and CLASS is
172# the class to instantiate if a match is found
173
Barry Warsaw0604d721999-04-26 23:17:16 +0000174FILETYPES = [
Barry Warsaw5c458052002-10-21 14:25:24 +0000175 (re.compile('Xorg'), RGBColorDB),
Barry Warsaw0604d721999-04-26 23:17:16 +0000176 (re.compile('XConsortium'), RGBColorDB),
177 (re.compile('HTML'), HTML40DB),
178 (re.compile('lightlink'), LightlinkDB),
179 (re.compile('Websafe'), WebsafeDB),
180 ]
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000181
Barry Warsaw0604d721999-04-26 23:17:16 +0000182def get_colordb(file, filetype=None):
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000183 colordb = None
Barry Warsaw0604d721999-04-26 23:17:16 +0000184 fp = open(file)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000185 try:
Barry Warsaw0604d721999-04-26 23:17:16 +0000186 line = fp.readline()
187 if not line:
188 return None
189 # try to determine the type of RGB file it is
190 if filetype is None:
191 filetypes = FILETYPES
192 else:
193 filetypes = [filetype]
194 for typere, class_ in filetypes:
195 mo = typere.search(line)
196 if mo:
197 break
198 else:
199 # no matching type
200 return None
201 # we know the type and the class to grok the type, so suck it in
202 colordb = class_(fp)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000203 finally:
Barry Warsaw0604d721999-04-26 23:17:16 +0000204 fp.close()
Barry Warsawf4562a71998-02-11 17:19:23 +0000205 # save a global copy
206 global DEFAULT_DB
207 DEFAULT_DB = colordb
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000208 return colordb
209
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000210
211
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000212_namedict = {}
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000213
214def rrggbb_to_triplet(color):
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000215 """Converts a #rrggbb color to the tuple (red, green, blue)."""
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000216 rgbtuple = _namedict.get(color)
217 if rgbtuple is None:
Barry Warsawb7b1cf01998-10-06 18:10:59 +0000218 if color[0] <> '#':
219 raise BadColor(color)
Barry Warsaw5c458052002-10-21 14:25:24 +0000220 red = color[1:3]
221 green = color[3:5]
222 blue = color[5:7]
Barry Warsaw6c50ae02001-07-10 21:38:47 +0000223 rgbtuple = int(red, 16), int(green, 16), int(blue, 16)
Barry Warsaw5c458052002-10-21 14:25:24 +0000224 _namedict[color] = rgbtuple
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000225 return rgbtuple
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000226
Barry Warsawf4562a71998-02-11 17:19:23 +0000227
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000228_tripdict = {}
Barry Warsawf4562a71998-02-11 17:19:23 +0000229def triplet_to_rrggbb(rgbtuple):
230 """Converts a (red, green, blue) tuple to #rrggbb."""
Barry Warsaw0604d721999-04-26 23:17:16 +0000231 global _tripdict
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000232 hexname = _tripdict.get(rgbtuple)
233 if hexname is None:
Barry Warsaw5c458052002-10-21 14:25:24 +0000234 hexname = '#%02x%02x%02x' % rgbtuple
235 _tripdict[rgbtuple] = hexname
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000236 return hexname
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000237
238
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000239_maxtuple = (256.0,) * 3
Barry Warsawa5a018f1998-09-25 22:51:36 +0000240def triplet_to_fractional_rgb(rgbtuple):
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000241 return map(operator.__div__, rgbtuple, _maxtuple)
Barry Warsaw2662e151998-02-13 21:27:56 +0000242
243
Barry Warsaw0e3e6991998-09-28 23:39:18 +0000244def triplet_to_brightness(rgbtuple):
245 # return the brightness (grey level) along the scale 0.0==black to
246 # 1.0==white
247 r = 0.299
248 g = 0.587
249 b = 0.114
250 return r*rgbtuple[0] + g*rgbtuple[1] + b*rgbtuple[2]
251
252
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000253
254if __name__ == '__main__':
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000255 colordb = get_colordb('/usr/openwin/lib/rgb.txt')
256 if not colordb:
Collin Winter6afaeb72007-08-03 17:06:41 +0000257 print('No parseable color database found')
Barry Warsaw5c458052002-10-21 14:25:24 +0000258 sys.exit(1)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000259 # on my system, this color matches exactly
260 target = 'navy'
Barry Warsawa5a018f1998-09-25 22:51:36 +0000261 red, green, blue = rgbtuple = colordb.find_byname(target)
Collin Winter6afaeb72007-08-03 17:06:41 +0000262 print(target, ':', red, green, blue, triplet_to_rrggbb(rgbtuple))
Barry Warsawa5a018f1998-09-25 22:51:36 +0000263 name, aliases = colordb.find_byrgb(rgbtuple)
Collin Winter6afaeb72007-08-03 17:06:41 +0000264 print('name:', name, 'aliases:', COMMASPACE.join(aliases))
Barry Warsaw5c458052002-10-21 14:25:24 +0000265 r, g, b = (1, 1, 128) # nearest to navy
266 r, g, b = (145, 238, 144) # nearest to lightgreen
267 r, g, b = (255, 251, 250) # snow
Collin Winter6afaeb72007-08-03 17:06:41 +0000268 print('finding nearest to', target, '...')
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000269 import time
270 t0 = time.time()
Barry Warsaw840a84d1998-10-15 02:18:08 +0000271 nearest = colordb.nearest(r, g, b)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000272 t1 = time.time()
Collin Winter6afaeb72007-08-03 17:06:41 +0000273 print('found nearest color', nearest, 'in', t1-t0, 'seconds')
Barry Warsaw840a84d1998-10-15 02:18:08 +0000274 # dump the database
275 for n in colordb.unique_names():
276 r, g, b = colordb.find_byname(n)
277 aliases = colordb.aliases_of(r, g, b)
Collin Winter6afaeb72007-08-03 17:06:41 +0000278 print('%20s: (%3d/%3d/%3d) == %s' % (n, r, g, b,
279 SPACE.join(aliases[1:])))