blob: fae0a3bed7d805af5853e47ca9993f94f05a9d39 [file] [log] [blame]
Barry Warsaw2406b1d1998-01-31 00:29:41 +00001"""Color Database.
2
3To create a class that contains color lookup methods, use the module global
4function `get_colordb(file)'. This function will try to examine the file to
5figure out what the format of the file is. If it can't figure out the file
6format, or it has trouble reading the file, None is returned. You can pass
7get_colordb() an optional filetype argument.
8
9Supporte file types are:
10
11 X_RGB_TXT -- X Consortium rgb.txt format files. Three columns of numbers
12 from 0 .. 255 separated by whitespace. Arbitrary trailing
13 columns used as the color name.
14"""
15
16import sys
Barry Warsaweb9b8af1998-02-11 18:55:37 +000017import string
Barry Warsaw2406b1d1998-01-31 00:29:41 +000018import re
Barry Warsawf4562a71998-02-11 17:19:23 +000019from types import *
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +000020import operator
Barry Warsaw2406b1d1998-01-31 00:29:41 +000021
Barry Warsaw9f4d73a1998-01-31 23:38:48 +000022class BadColor(Exception):
23 pass
24
Barry Warsawf4562a71998-02-11 17:19:23 +000025DEFAULT_DB = None
26
Barry Warsawa5a018f1998-09-25 22:51:36 +000027
Barry Warsaw2406b1d1998-01-31 00:29:41 +000028
29# generic class
30class ColorDB:
31 def __init__(self, fp, lineno):
32 # Maintain several dictionaries for indexing into the color database.
33 # Note that while Tk supports RGB intensities of 4, 8, 12, or 16 bits,
34 # for now we only support 8 bit intensities. At least on OpenWindows,
35 # all intensities in the /usr/openwin/lib/rgb.txt file are 8-bit
36 #
Barry Warsaweb9b8af1998-02-11 18:55:37 +000037 # key is (red, green, blue) tuple, value is (name, [aliases])
38 self.__byrgb = {}
Barry Warsaw2406b1d1998-01-31 00:29:41 +000039 #
Barry Warsaweb9b8af1998-02-11 18:55:37 +000040 # key is name, value is (red, green, blue)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000041 self.__byname = {}
42 #
43 while 1:
44 line = fp.readline()
45 if not line:
46 break
47 # get this compiled regular expression from derived class
48 mo = self._re.match(line)
49 if not mo:
50 sys.stderr.write('Error in %s, line %d\n' % (fp.name, lineno))
51 lineno = lineno + 1
52 continue
53 #
54 # extract the red, green, blue, and name
Barry Warsaweb9b8af1998-02-11 18:55:37 +000055 #
Barry Warsaw2406b1d1998-01-31 00:29:41 +000056 red, green, blue = map(int, mo.group('red', 'green', 'blue'))
57 name = mo.group('name')
Barry Warsaweb9b8af1998-02-11 18:55:37 +000058 keyname = string.lower(name)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000059 #
60 # TBD: for now the `name' is just the first named color with the
61 # rgb values we find. Later, we might want to make the two word
62 # version the `name', or the CapitalizedVersion, etc.
Barry Warsaweb9b8af1998-02-11 18:55:37 +000063 #
64 key = (red, green, blue)
65 foundname, aliases = self.__byrgb.get(key, (name, []))
Barry Warsaw2406b1d1998-01-31 00:29:41 +000066 if foundname <> name and foundname not in aliases:
67 aliases.append(name)
Barry Warsaweb9b8af1998-02-11 18:55:37 +000068 self.__byrgb[key] = (foundname, aliases)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000069 #
70 # add to byname lookup
Barry Warsaweb9b8af1998-02-11 18:55:37 +000071 #
72 self.__byname[keyname] = key
Barry Warsaw2406b1d1998-01-31 00:29:41 +000073 lineno = lineno + 1
74
Barry Warsaweb9b8af1998-02-11 18:55:37 +000075 def find_byrgb(self, rgbtuple):
Barry Warsaw9f4d73a1998-01-31 23:38:48 +000076 try:
Barry Warsaweb9b8af1998-02-11 18:55:37 +000077 return self.__byrgb[rgbtuple]
Barry Warsaw9f4d73a1998-01-31 23:38:48 +000078 except KeyError:
Barry Warsaweb9b8af1998-02-11 18:55:37 +000079 raise BadColor(rgbtuple)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000080
81 def find_byname(self, name):
Barry Warsaweb9b8af1998-02-11 18:55:37 +000082 name = string.lower(name)
Barry Warsaw9f4d73a1998-01-31 23:38:48 +000083 try:
84 return self.__byname[name]
85 except KeyError:
86 raise BadColor(name)
Barry Warsaw2406b1d1998-01-31 00:29:41 +000087
Barry Warsawf4562a71998-02-11 17:19:23 +000088 def nearest(self, rgbtuple):
Barry Warsaw2406b1d1998-01-31 00:29:41 +000089 # TBD: use Voronoi diagrams, Delaunay triangulation, or octree for
Barry Warsaweb9b8af1998-02-11 18:55:37 +000090 # speeding up the locating of nearest point. Exhaustive search is
91 # inefficient, but may be fast enough.
Barry Warsawf4562a71998-02-11 17:19:23 +000092 red, green, blue = rgbtuple
Barry Warsaw2406b1d1998-01-31 00:29:41 +000093 nearest = -1
94 nearest_name = ''
Barry Warsaweb9b8af1998-02-11 18:55:37 +000095 for name, aliases in self.__byrgb.values():
96 r, g, b = self.__byname[string.lower(name)]
Barry Warsaw2406b1d1998-01-31 00:29:41 +000097 rdelta = red - r
98 gdelta = green - g
99 bdelta = blue - b
100 distance = rdelta * rdelta + gdelta * gdelta + bdelta * bdelta
101 if nearest == -1 or distance < nearest:
102 nearest = distance
103 nearest_name = name
104 return nearest_name
105
106
107class RGBColorDB(ColorDB):
108 _re = re.compile(
109 '\s*(?P<red>\d+)\s+(?P<green>\d+)\s+(?P<blue>\d+)\s+(?P<name>.*)')
110
111
112
113# format is a tuple (RE, SCANLINES, CLASS) where RE is a compiled regular
114# expression, SCANLINES is the number of header lines to scan, and CLASS is
115# the class to instantiate if a match is found
116
117X_RGB_TXT = re.compile('XConsortium'), 1, RGBColorDB
118
119def get_colordb(file, filetype=X_RGB_TXT):
120 colordb = None
121 fp = None
122 typere, scanlines, class_ = filetype
123 try:
124 try:
125 lineno = 0
126 fp = open(file)
127 while lineno < scanlines:
128 line = fp.readline()
129 if not line:
130 break
131 mo = typere.search(line)
132 if mo:
133 colordb = class_(fp, lineno)
134 break
135 lineno = lineno + 1
136 except IOError:
137 pass
138 finally:
139 if fp:
140 fp.close()
Barry Warsawf4562a71998-02-11 17:19:23 +0000141 # save a global copy
142 global DEFAULT_DB
143 DEFAULT_DB = colordb
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000144 return colordb
145
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000146
147
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000148_namedict = {}
Barry Warsaw2e7a3201998-02-18 17:01:12 +0000149def rrggbb_to_triplet(color, atoi=string.atoi):
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000150 """Converts a #rrggbb color to the tuple (red, green, blue)."""
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000151 rgbtuple = _namedict.get(color)
152 if rgbtuple is None:
153 assert color[0] == '#'
154 red = color[1:3]
155 green = color[3:5]
156 blue = color[5:7]
Barry Warsaw2e7a3201998-02-18 17:01:12 +0000157 rgbtuple = (atoi(red, 16), atoi(green, 16), atoi(blue, 16))
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000158 _namedict[color] = rgbtuple
159 return rgbtuple
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000160
Barry Warsawf4562a71998-02-11 17:19:23 +0000161
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000162_tripdict = {}
Barry Warsawf4562a71998-02-11 17:19:23 +0000163def triplet_to_rrggbb(rgbtuple):
164 """Converts a (red, green, blue) tuple to #rrggbb."""
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000165 hexname = _tripdict.get(rgbtuple)
166 if hexname is None:
Barry Warsaw2e7a3201998-02-18 17:01:12 +0000167 hexname = '#%02x%02x%02x' % rgbtuple
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000168 _tripdict[rgbtuple] = hexname
169 return hexname
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000170
171
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000172_maxtuple = (256.0,) * 3
Barry Warsawa5a018f1998-09-25 22:51:36 +0000173def triplet_to_fractional_rgb(rgbtuple):
Barry Warsaw8d3e5ee1998-02-18 00:02:26 +0000174 return map(operator.__div__, rgbtuple, _maxtuple)
Barry Warsaw2662e151998-02-13 21:27:56 +0000175
176
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000177
178if __name__ == '__main__':
179 import string
180
181 colordb = get_colordb('/usr/openwin/lib/rgb.txt')
182 if not colordb:
183 print 'No parseable color database found'
184 sys.exit(1)
185 # on my system, this color matches exactly
186 target = 'navy'
Barry Warsawa5a018f1998-09-25 22:51:36 +0000187 red, green, blue = rgbtuple = colordb.find_byname(target)
188 print target, ':', red, green, blue, triplet_to_rrggbb(rgbtuple)
189 name, aliases = colordb.find_byrgb(rgbtuple)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000190 print 'name:', name, 'aliases:', string.join(aliases, ", ")
191 target = (1, 1, 128) # nearest to navy
192 target = (145, 238, 144) # nearest to lightgreen
193 target = (255, 251, 250) # snow
194 print 'finding nearest to', target, '...'
195 import time
196 t0 = time.time()
Barry Warsawa5a018f1998-09-25 22:51:36 +0000197 nearest = colordb.nearest(target)
Barry Warsaw2406b1d1998-01-31 00:29:41 +0000198 t1 = time.time()
199 print 'found nearest color', nearest, 'in', t1-t0, 'seconds'
Barry Warsaw9f4d73a1998-01-31 23:38:48 +0000200