blob: 87c440608605e1a6090a62cf6d4c78653193c401 [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
Barry Warsaweb9b8af1998-02-11 18:55:37 +000024import string
Barry Warsaw2406b1d1998-01-31 00:29:41 +000025import re
Barry Warsawf4562a71998-02-11 17:19:23 +000026from types import *
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +000027import operator
Barry Warsaw2406b1d1998-01-31 00:29:41 +000028
Barry Warsaw9f4d73a1998-01-31 23:38:48 +000029class BadColor(Exception):
30 pass
31
Barry Warsawf4562a71998-02-11 17:19:23 +000032DEFAULT_DB = None
33
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 Warsaw2406b1d1998-01-31 00:29:41 +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 #
Barry Warsaweb9b8af1998-02-11 18:55:37 +000046 # key is (red, green, blue) tuple, value is (name, [aliases])
47 self.__byrgb = {}
Barry Warsaw2406b1d1998-01-31 00:29:41 +000048 #
Barry Warsaweb9b8af1998-02-11 18:55:37 +000049 # key is name, value is (red, green, blue)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000050 self.__byname = {}
51 #
Barry Warsaw8be25941998-10-02 14:43:30 +000052 # all unique names (non-aliases). built-on demand
53 self.__allnames = None
Barry Warsaw2406b1d1998-01-31 00:29:41 +000054 while 1:
55 line = fp.readline()
56 if not line:
57 break
58 # get this compiled regular expression from derived class
Barry Warsaw0604d721999-04-26 23:17:16 +000059## print '%3d: %s' % (lineno, line[:-1])
Barry Warsaw2406b1d1998-01-31 00:29:41 +000060 mo = self._re.match(line)
61 if not mo:
62 sys.stderr.write('Error in %s, line %d\n' % (fp.name, lineno))
63 lineno = lineno + 1
64 continue
65 #
66 # extract the red, green, blue, and name
Barry Warsaweb9b8af1998-02-11 18:55:37 +000067 #
Barry Warsaw0604d721999-04-26 23:17:16 +000068 red, green, blue = self._extractrgb(mo)
69 name = self._extractname(mo)
Barry Warsaweb9b8af1998-02-11 18:55:37 +000070 keyname = string.lower(name)
Barry Warsaw0604d721999-04-26 23:17:16 +000071## print keyname, '(%d, %d, %d)' % (red, green, blue)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000072 #
73 # TBD: for now the `name' is just the first named color with the
74 # rgb values we find. Later, we might want to make the two word
75 # version the `name', or the CapitalizedVersion, etc.
Barry Warsaweb9b8af1998-02-11 18:55:37 +000076 #
77 key = (red, green, blue)
78 foundname, aliases = self.__byrgb.get(key, (name, []))
Barry Warsaw2406b1d1998-01-31 00:29:41 +000079 if foundname <> name and foundname not in aliases:
80 aliases.append(name)
Barry Warsaweb9b8af1998-02-11 18:55:37 +000081 self.__byrgb[key] = (foundname, aliases)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000082 #
83 # add to byname lookup
Barry Warsaweb9b8af1998-02-11 18:55:37 +000084 #
85 self.__byname[keyname] = key
Barry Warsaw2406b1d1998-01-31 00:29:41 +000086 lineno = lineno + 1
87
Barry Warsaw0604d721999-04-26 23:17:16 +000088 # override in derived classes
89 def _extractrgb(self, mo):
90 return map(int, mo.group('red', 'green', 'blue'))
91
92 def _extractname(self, mo):
93 return mo.group('name')
94
95 def filename(self):
96 return self.__name
97
Barry Warsaweb9b8af1998-02-11 18:55:37 +000098 def find_byrgb(self, rgbtuple):
Barry Warsaw0604d721999-04-26 23:17:16 +000099 """Return name for rgbtuple"""
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000100 try:
Barry Warsaweb9b8af1998-02-11 18:55:37 +0000101 return self.__byrgb[rgbtuple]
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000102 except KeyError:
Barry Warsaweb9b8af1998-02-11 18:55:37 +0000103 raise BadColor(rgbtuple)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000104
105 def find_byname(self, name):
Barry Warsaw0604d721999-04-26 23:17:16 +0000106 """Return (red, green, blue) for name"""
Barry Warsaweb9b8af1998-02-11 18:55:37 +0000107 name = string.lower(name)
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000108 try:
109 return self.__byname[name]
110 except KeyError:
111 raise BadColor(name)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000112
Barry Warsaw7a134181998-09-29 20:03:15 +0000113 def nearest(self, red, green, blue):
Barry Warsaw0604d721999-04-26 23:17:16 +0000114 """Return the name of color nearest (red, green, blue)"""
115 # TBD: should we use Voronoi diagrams, Delaunay triangulation, or
116 # octree for speeding up the locating of nearest point? Exhaustive
117 # search is inefficient, but seems fast enough.
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000118 nearest = -1
119 nearest_name = ''
Barry Warsaweb9b8af1998-02-11 18:55:37 +0000120 for name, aliases in self.__byrgb.values():
121 r, g, b = self.__byname[string.lower(name)]
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000122 rdelta = red - r
123 gdelta = green - g
124 bdelta = blue - b
125 distance = rdelta * rdelta + gdelta * gdelta + bdelta * bdelta
126 if nearest == -1 or distance < nearest:
127 nearest = distance
128 nearest_name = name
129 return nearest_name
Barry Warsaw8be25941998-10-02 14:43:30 +0000130
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000131 def unique_names(self):
Barry Warsaw8be25941998-10-02 14:43:30 +0000132 # sorted
133 if not self.__allnames:
134 self.__allnames = []
135 for name, aliases in self.__byrgb.values():
136 self.__allnames.append(name)
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000137 # sort irregardless of case
138 def nocase_cmp(n1, n2):
139 return cmp(string.lower(n1), string.lower(n2))
140 self.__allnames.sort(nocase_cmp)
Barry Warsaw8be25941998-10-02 14:43:30 +0000141 return self.__allnames
Barry Warsaw9f3ea211998-10-02 15:59:20 +0000142
143 def aliases_of(self, red, green, blue):
144 try:
145 name, aliases = self.__byrgb[(red, green, blue)]
146 except KeyError:
147 raise BadColor((red, green, blue))
148 return [name] + aliases
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000149
150
151class RGBColorDB(ColorDB):
152 _re = re.compile(
Barry Warsaw0604d721999-04-26 23:17:16 +0000153 '\s*(?P<red>\d+)\s+(?P<green>\d+)\s+(?P<blue>\d+)\s+(?P<name>.*)')
154
155
156class HTML40DB(ColorDB):
157 _re = re.compile('(?P<name>\S+)\s+(?P<hexrgb>#[0-9a-fA-F]{6})')
158
159 def _extractrgb(self, mo):
160 return rrggbb_to_triplet(mo.group('hexrgb'))
161
162class LightlinkDB(HTML40DB):
163 _re = re.compile('(?P<name>(.+))\s+(?P<hexrgb>#[0-9a-fA-F]{6})')
164
165 def _extractname(self, mo):
166 return string.strip(mo.group('name'))
167
168class WebsafeDB(ColorDB):
169 _re = re.compile('(?P<hexrgb>#[0-9a-fA-F]{6})')
170
171 def _extractrgb(self, mo):
172 return rrggbb_to_triplet(mo.group('hexrgb'))
173
174 def _extractname(self, mo):
175 return string.upper(mo.group('hexrgb'))
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000176
177
178
179# format is a tuple (RE, SCANLINES, CLASS) where RE is a compiled regular
180# expression, SCANLINES is the number of header lines to scan, and CLASS is
181# the class to instantiate if a match is found
182
Barry Warsaw0604d721999-04-26 23:17:16 +0000183FILETYPES = [
184 (re.compile('XConsortium'), RGBColorDB),
185 (re.compile('HTML'), HTML40DB),
186 (re.compile('lightlink'), LightlinkDB),
187 (re.compile('Websafe'), WebsafeDB),
188 ]
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000189
Barry Warsaw0604d721999-04-26 23:17:16 +0000190def get_colordb(file, filetype=None):
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000191 colordb = None
Barry Warsaw0604d721999-04-26 23:17:16 +0000192 fp = open(file)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000193 try:
Barry Warsaw0604d721999-04-26 23:17:16 +0000194 line = fp.readline()
195 if not line:
196 return None
197 # try to determine the type of RGB file it is
198 if filetype is None:
199 filetypes = FILETYPES
200 else:
201 filetypes = [filetype]
202 for typere, class_ in filetypes:
203 mo = typere.search(line)
204 if mo:
205 break
206 else:
207 # no matching type
208 return None
209 # we know the type and the class to grok the type, so suck it in
210 colordb = class_(fp)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000211 finally:
Barry Warsaw0604d721999-04-26 23:17:16 +0000212 fp.close()
Barry Warsawf4562a71998-02-11 17:19:23 +0000213 # save a global copy
214 global DEFAULT_DB
215 DEFAULT_DB = colordb
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000216 return colordb
217
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000218
219
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000220_namedict = {}
Barry Warsaw2e7a3201998-02-18 17:01:12 +0000221def rrggbb_to_triplet(color, atoi=string.atoi):
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000222 """Converts a #rrggbb color to the tuple (red, green, blue)."""
Barry Warsaw0604d721999-04-26 23:17:16 +0000223 global _namedict
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000224 rgbtuple = _namedict.get(color)
225 if rgbtuple is None:
Barry Warsawb7b1cf01998-10-06 18:10:59 +0000226 if color[0] <> '#':
227 raise BadColor(color)
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000228 red = color[1:3]
229 green = color[3:5]
230 blue = color[5:7]
Barry Warsaw2e7a3201998-02-18 17:01:12 +0000231 rgbtuple = (atoi(red, 16), atoi(green, 16), atoi(blue, 16))
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000232 _namedict[color] = rgbtuple
233 return rgbtuple
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000234
Barry Warsawf4562a71998-02-11 17:19:23 +0000235
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000236_tripdict = {}
Barry Warsawf4562a71998-02-11 17:19:23 +0000237def triplet_to_rrggbb(rgbtuple):
238 """Converts a (red, green, blue) tuple to #rrggbb."""
Barry Warsaw0604d721999-04-26 23:17:16 +0000239 global _tripdict
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000240 hexname = _tripdict.get(rgbtuple)
241 if hexname is None:
Barry Warsaw2e7a3201998-02-18 17:01:12 +0000242 hexname = '#%02x%02x%02x' % rgbtuple
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000243 _tripdict[rgbtuple] = hexname
244 return hexname
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000245
246
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000247_maxtuple = (256.0,) * 3
Barry Warsawa5a018f1998-09-25 22:51:36 +0000248def triplet_to_fractional_rgb(rgbtuple):
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000249 return map(operator.__div__, rgbtuple, _maxtuple)
Barry Warsaw2662e151998-02-13 21:27:56 +0000250
251
Barry Warsaw0e3e6991998-09-28 23:39:18 +0000252def triplet_to_brightness(rgbtuple):
253 # return the brightness (grey level) along the scale 0.0==black to
254 # 1.0==white
255 r = 0.299
256 g = 0.587
257 b = 0.114
258 return r*rgbtuple[0] + g*rgbtuple[1] + b*rgbtuple[2]
259
260
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000261
262if __name__ == '__main__':
263 import string
264
265 colordb = get_colordb('/usr/openwin/lib/rgb.txt')
266 if not colordb:
267 print 'No parseable color database found'
268 sys.exit(1)
269 # on my system, this color matches exactly
270 target = 'navy'
Barry Warsawa5a018f1998-09-25 22:51:36 +0000271 red, green, blue = rgbtuple = colordb.find_byname(target)
272 print target, ':', red, green, blue, triplet_to_rrggbb(rgbtuple)
273 name, aliases = colordb.find_byrgb(rgbtuple)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000274 print 'name:', name, 'aliases:', string.join(aliases, ", ")
Barry Warsaw840a84d1998-10-15 02:18:08 +0000275 r, g, b = (1, 1, 128) # nearest to navy
276 r, g, b = (145, 238, 144) # nearest to lightgreen
277 r, g, b = (255, 251, 250) # snow
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000278 print 'finding nearest to', target, '...'
279 import time
280 t0 = time.time()
Barry Warsaw840a84d1998-10-15 02:18:08 +0000281 nearest = colordb.nearest(r, g, b)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000282 t1 = time.time()
283 print 'found nearest color', nearest, 'in', t1-t0, 'seconds'
Barry Warsaw840a84d1998-10-15 02:18:08 +0000284 # dump the database
285 for n in colordb.unique_names():
286 r, g, b = colordb.find_byname(n)
287 aliases = colordb.aliases_of(r, g, b)
288 print '%20s: (%3d/%3d/%3d) == %s' % (n, r, g, b,
289 string.join(aliases[1:]))