Move setting of ioready 'wait' earlier in call chain, to
[python/dscho.git] / Tools / pynche / ColorDB.py
blob96b6ce67c85dd48a227d24fb55cd1c424aea9ec8
1 """Color Database.
3 This file contains one class, called ColorDB, and several utility functions.
4 The class must be instantiated by the get_colordb() function in this file,
5 passing it a filename to read a database out of.
7 The get_colordb() function will try to examine the file to figure out what the
8 format of the file is. If it can't figure out the file format, or it has
9 trouble reading the file, None is returned. You can pass get_colordb() an
10 optional filetype argument.
12 Supporte file types are:
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.
18 The utility functions are useful for converting between the various expected
19 color formats, and for calculating other color values.
21 """
23 import sys
24 import re
25 from types import *
26 import operator
28 class BadColor(Exception):
29 pass
31 DEFAULT_DB = None
32 SPACE = ' '
33 COMMASPACE = ', '
37 # generic class
38 class ColorDB:
39 def __init__(self, fp):
40 lineno = 2
41 self.__name = fp.name
42 # 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
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 = {}
51 # all unique names (non-aliases). built-on demand
52 self.__allnames = None
53 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:
60 print >> sys.stderr, 'Error in', fp.name, ' line', lineno
61 lineno += 1
62 continue
63 # extract the red, green, blue, and name
64 red, green, blue = self._extractrgb(mo)
65 name = self._extractname(mo)
66 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
79 # override in derived classes
80 def _extractrgb(self, mo):
81 return [int(x) for x in mo.group('red', 'green', 'blue')]
83 def _extractname(self, mo):
84 return mo.group('name')
86 def filename(self):
87 return self.__name
89 def find_byrgb(self, rgbtuple):
90 """Return name for rgbtuple"""
91 try:
92 return self.__byrgb[rgbtuple]
93 except KeyError:
94 raise BadColor(rgbtuple)
96 def find_byname(self, name):
97 """Return (red, green, blue) for name"""
98 name = name.lower()
99 try:
100 return self.__byname[name]
101 except KeyError:
102 raise BadColor(name)
104 def nearest(self, red, green, blue):
105 """Return the name of color nearest (red, green, blue)"""
106 # 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
122 def unique_names(self):
123 # sorted
124 if not self.__allnames:
125 self.__allnames = []
126 for name, aliases in self.__byrgb.values():
127 self.__allnames.append(name)
128 # sort irregardless of case
129 def nocase_cmp(n1, n2):
130 return cmp(n1.lower(), n2.lower())
131 self.__allnames.sort(nocase_cmp)
132 return self.__allnames
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
142 class RGBColorDB(ColorDB):
143 _re = re.compile(
144 '\s*(?P<red>\d+)\s+(?P<green>\d+)\s+(?P<blue>\d+)\s+(?P<name>.*)')
147 class HTML40DB(ColorDB):
148 _re = re.compile('(?P<name>\S+)\s+(?P<hexrgb>#[0-9a-fA-F]{6})')
150 def _extractrgb(self, mo):
151 return rrggbb_to_triplet(mo.group('hexrgb'))
153 class LightlinkDB(HTML40DB):
154 _re = re.compile('(?P<name>(.+))\s+(?P<hexrgb>#[0-9a-fA-F]{6})')
156 def _extractname(self, mo):
157 return mo.group('name').strip()
159 class WebsafeDB(ColorDB):
160 _re = re.compile('(?P<hexrgb>#[0-9a-fA-F]{6})')
162 def _extractrgb(self, mo):
163 return rrggbb_to_triplet(mo.group('hexrgb'))
165 def _extractname(self, mo):
166 return mo.group('hexrgb').upper()
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
174 FILETYPES = [
175 (re.compile('Xorg'), RGBColorDB),
176 (re.compile('XConsortium'), RGBColorDB),
177 (re.compile('HTML'), HTML40DB),
178 (re.compile('lightlink'), LightlinkDB),
179 (re.compile('Websafe'), WebsafeDB),
182 def get_colordb(file, filetype=None):
183 colordb = None
184 fp = open(file)
185 try:
186 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)
203 finally:
204 fp.close()
205 # save a global copy
206 global DEFAULT_DB
207 DEFAULT_DB = colordb
208 return colordb
212 _namedict = {}
214 def rrggbb_to_triplet(color):
215 """Converts a #rrggbb color to the tuple (red, green, blue)."""
216 rgbtuple = _namedict.get(color)
217 if rgbtuple is None:
218 if color[0] <> '#':
219 raise BadColor(color)
220 red = color[1:3]
221 green = color[3:5]
222 blue = color[5:7]
223 rgbtuple = int(red, 16), int(green, 16), int(blue, 16)
224 _namedict[color] = rgbtuple
225 return rgbtuple
228 _tripdict = {}
229 def triplet_to_rrggbb(rgbtuple):
230 """Converts a (red, green, blue) tuple to #rrggbb."""
231 global _tripdict
232 hexname = _tripdict.get(rgbtuple)
233 if hexname is None:
234 hexname = '#%02x%02x%02x' % rgbtuple
235 _tripdict[rgbtuple] = hexname
236 return hexname
239 _maxtuple = (256.0,) * 3
240 def triplet_to_fractional_rgb(rgbtuple):
241 return map(operator.__div__, rgbtuple, _maxtuple)
244 def 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]
254 if __name__ == '__main__':
255 colordb = get_colordb('/usr/openwin/lib/rgb.txt')
256 if not colordb:
257 print 'No parseable color database found'
258 sys.exit(1)
259 # on my system, this color matches exactly
260 target = 'navy'
261 red, green, blue = rgbtuple = colordb.find_byname(target)
262 print target, ':', red, green, blue, triplet_to_rrggbb(rgbtuple)
263 name, aliases = colordb.find_byrgb(rgbtuple)
264 print 'name:', name, 'aliases:', COMMASPACE.join(aliases)
265 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
268 print 'finding nearest to', target, '...'
269 import time
270 t0 = time.time()
271 nearest = colordb.nearest(r, g, b)
272 t1 = time.time()
273 print 'found nearest color', nearest, 'in', t1-t0, 'seconds'
274 # 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)
278 print '%20s: (%3d/%3d/%3d) == %s' % (n, r, g, b,
279 SPACE.join(aliases[1:]))