1"""Color Database.
2
3This 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.
11
12Supported 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.
17
18The utility functions are useful for converting between the various expected
19color formats, and for calculating other color values.
20
21"""
22
23import sys
24import re
25from types import *
26
27class BadColor(Exception):
28    pass
29
30DEFAULT_DB = None
31SPACE = ' '
32COMMASPACE = ', '
33
34
35
36# generic class
37class ColorDB:
38    def __init__(self, fp):
39        lineno = 2
40        self.__name = fp.name
41        # 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        #
46        # key is (red, green, blue) tuple, value is (name, [aliases])
47        self.__byrgb = {}
48        # key is name, value is (red, green, blue)
49        self.__byname = {}
50        # all unique names (non-aliases).  built-on demand
51        self.__allnames = None
52        for line in fp:
53            # get this compiled regular expression from derived class
54            mo = self._re.match(line)
55            if not mo:
56                print('Error in', fp.name, ' line', lineno, file=sys.stderr)
57                lineno += 1
58                continue
59            # extract the red, green, blue, and name
60            red, green, blue = self._extractrgb(mo)
61            name = self._extractname(mo)
62            keyname = name.lower()
63            # BAW: for now the `name' is just the first named color with the
64            # rgb values we find.  Later, we might want to make the two word
65            # version the `name', or the CapitalizedVersion, etc.
66            key = (red, green, blue)
67            foundname, aliases = self.__byrgb.get(key, (name, []))
68            if foundname != name and foundname not in aliases:
69                aliases.append(name)
70            self.__byrgb[key] = (foundname, aliases)
71            # add to byname lookup
72            self.__byname[keyname] = key
73            lineno = lineno + 1
74
75    # override in derived classes
76    def _extractrgb(self, mo):
77        return [int(x) for x in mo.group('red', 'green', 'blue')]
78
79    def _extractname(self, mo):
80        return mo.group('name')
81
82    def filename(self):
83        return self.__name
84
85    def find_byrgb(self, rgbtuple):
86        """Return name for rgbtuple"""
87        try:
88            return self.__byrgb[rgbtuple]
89        except KeyError:
90            raise BadColor(rgbtuple) from None
91
92    def find_byname(self, name):
93        """Return (red, green, blue) for name"""
94        name = name.lower()
95        try:
96            return self.__byname[name]
97        except KeyError:
98            raise BadColor(name) from None
99
100    def nearest(self, red, green, blue):
101        """Return the name of color nearest (red, green, blue)"""
102        # BAW: should we use Voronoi diagrams, Delaunay triangulation, or
103        # octree for speeding up the locating of nearest point?  Exhaustive
104        # search is inefficient, but seems fast enough.
105        nearest = -1
106        nearest_name = ''
107        for name, aliases in self.__byrgb.values():
108            r, g, b = self.__byname[name.lower()]
109            rdelta = red - r
110            gdelta = green - g
111            bdelta = blue - b
112            distance = rdelta * rdelta + gdelta * gdelta + bdelta * bdelta
113            if nearest == -1 or distance < nearest:
114                nearest = distance
115                nearest_name = name
116        return nearest_name
117
118    def unique_names(self):
119        # sorted
120        if not self.__allnames:
121            self.__allnames = []
122            for name, aliases in self.__byrgb.values():
123                self.__allnames.append(name)
124            self.__allnames.sort(key=str.lower)
125        return self.__allnames
126
127    def aliases_of(self, red, green, blue):
128        try:
129            name, aliases = self.__byrgb[(red, green, blue)]
130        except KeyError:
131            raise BadColor((red, green, blue)) from None
132        return [name] + aliases
133
134
135class RGBColorDB(ColorDB):
136    _re = re.compile(
137        r'\s*(?P<red>\d+)\s+(?P<green>\d+)\s+(?P<blue>\d+)\s+(?P<name>.*)')
138
139
140class HTML40DB(ColorDB):
141    _re = re.compile(r'(?P<name>\S+)\s+(?P<hexrgb>#[0-9a-fA-F]{6})')
142
143    def _extractrgb(self, mo):
144        return rrggbb_to_triplet(mo.group('hexrgb'))
145
146class LightlinkDB(HTML40DB):
147    _re = re.compile(r'(?P<name>(.+))\s+(?P<hexrgb>#[0-9a-fA-F]{6})')
148
149    def _extractname(self, mo):
150        return mo.group('name').strip()
151
152class WebsafeDB(ColorDB):
153    _re = re.compile('(?P<hexrgb>#[0-9a-fA-F]{6})')
154
155    def _extractrgb(self, mo):
156        return rrggbb_to_triplet(mo.group('hexrgb'))
157
158    def _extractname(self, mo):
159        return mo.group('hexrgb').upper()
160
161
162
163# format is a tuple (RE, SCANLINES, CLASS) where RE is a compiled regular
164# expression, SCANLINES is the number of header lines to scan, and CLASS is
165# the class to instantiate if a match is found
166
167FILETYPES = [
168    (re.compile('Xorg'), RGBColorDB),
169    (re.compile('XConsortium'), RGBColorDB),
170    (re.compile('HTML'), HTML40DB),
171    (re.compile('lightlink'), LightlinkDB),
172    (re.compile('Websafe'), WebsafeDB),
173    ]
174
175def get_colordb(file, filetype=None):
176    colordb = None
177    fp = open(file)
178    try:
179        line = fp.readline()
180        if not line:
181            return None
182        # try to determine the type of RGB file it is
183        if filetype is None:
184            filetypes = FILETYPES
185        else:
186            filetypes = [filetype]
187        for typere, class_ in filetypes:
188            mo = typere.search(line)
189            if mo:
190                break
191        else:
192            # no matching type
193            return None
194        # we know the type and the class to grok the type, so suck it in
195        colordb = class_(fp)
196    finally:
197        fp.close()
198    # save a global copy
199    global DEFAULT_DB
200    DEFAULT_DB = colordb
201    return colordb
202
203
204
205_namedict = {}
206
207def rrggbb_to_triplet(color):
208    """Converts a #rrggbb color to the tuple (red, green, blue)."""
209    rgbtuple = _namedict.get(color)
210    if rgbtuple is None:
211        if color[0] != '#':
212            raise BadColor(color)
213        red = color[1:3]
214        green = color[3:5]
215        blue = color[5:7]
216        rgbtuple = int(red, 16), int(green, 16), int(blue, 16)
217        _namedict[color] = rgbtuple
218    return rgbtuple
219
220
221_tripdict = {}
222def triplet_to_rrggbb(rgbtuple):
223    """Converts a (red, green, blue) tuple to #rrggbb."""
224    global _tripdict
225    hexname = _tripdict.get(rgbtuple)
226    if hexname is None:
227        hexname = '#%02x%02x%02x' % rgbtuple
228        _tripdict[rgbtuple] = hexname
229    return hexname
230
231
232def triplet_to_fractional_rgb(rgbtuple):
233    return [x / 256 for x in rgbtuple]
234
235
236def triplet_to_brightness(rgbtuple):
237    # return the brightness (grey level) along the scale 0.0==black to
238    # 1.0==white
239    r = 0.299
240    g = 0.587
241    b = 0.114
242    return r*rgbtuple[0] + g*rgbtuple[1] + b*rgbtuple[2]
243
244
245
246if __name__ == '__main__':
247    colordb = get_colordb('/usr/openwin/lib/rgb.txt')
248    if not colordb:
249        print('No parseable color database found')
250        sys.exit(1)
251    # on my system, this color matches exactly
252    target = 'navy'
253    red, green, blue = rgbtuple = colordb.find_byname(target)
254    print(target, ':', red, green, blue, triplet_to_rrggbb(rgbtuple))
255    name, aliases = colordb.find_byrgb(rgbtuple)
256    print('name:', name, 'aliases:', COMMASPACE.join(aliases))
257    r, g, b = (1, 1, 128)                         # nearest to navy
258    r, g, b = (145, 238, 144)                     # nearest to lightgreen
259    r, g, b = (255, 251, 250)                     # snow
260    print('finding nearest to', target, '...')
261    import time
262    t0 = time.time()
263    nearest = colordb.nearest(r, g, b)
264    t1 = time.time()
265    print('found nearest color', nearest, 'in', t1-t0, 'seconds')
266    # dump the database
267    for n in colordb.unique_names():
268        r, g, b = colordb.find_byname(n)
269        aliases = colordb.aliases_of(r, g, b)
270        print('%20s: (%3d/%3d/%3d) == %s' % (n, r, g, b,
271                                             SPACE.join(aliases[1:])))
272