ColorDB.py 8.6 KB

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