Advertisement
IDTST

GetMostUsedImageColourCodesPython

Nov 24th, 2014
285
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 18.65 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3. """
  4. Theme generator/manager.
  5.  
  6. Can generate themes from:
  7.  
  8. - Xresources-style color files
  9. - Sweyla's site, e.g. for http://sweyla.com/themes/seed/693812/ -> 693812
  10. - Images, in which case it will use k-means to get colors
  11.  
  12. Requires:
  13.  
  14. - ~/.config/themer/templates/ directory w/one valid set of templates
  15.  
  16. Assumes:
  17.  
  18. - you're using "any color you like" icon set
  19. - probably a lot of other things
  20. """
  21.  
  22. from collections import namedtuple
  23. import colorsys
  24. import itertools
  25. import logging
  26. import math
  27. import optparse
  28. import os
  29. import random
  30. import re
  31. import shutil
  32. import sys
  33. import urllib2
  34. import yaml
  35.  
  36. from jinja2 import Environment, FileSystemLoader
  37. try:
  38.     import Image, ImageDraw
  39. except ImportError:
  40.     from PIL import Image, ImageDraw
  41.  
  42. logger = logging.getLogger(__name__)
  43.  
  44.  
  45. DEFAULT_CONTEXT_CONFIG = {
  46.     'primary': 'magenta',
  47.     'secondary': 'green',
  48.     'tertiary': 'blue',
  49. }
  50. CONFIG_DIR = os.getenv('XDG_CONFIG_HOME', os.path.join(os.getenv('HOME'), '.config'))
  51. THEMER_ROOT = os.path.join(CONFIG_DIR, 'themer')
  52. TEMPLATE_ROOT = os.path.join(THEMER_ROOT, 'templates')
  53.  
  54. def dict_update(parent, child):
  55.     """Recursively update parent dict with child dict."""
  56.     for key, value in child.iteritems():
  57.         if key in parent and isinstance(parent[key], dict):
  58.             parent[key] = dict_update(parent[key], value)
  59.         else:
  60.             parent[key] = value
  61.     return parent
  62.  
  63. def read_config(config_file):
  64.     """Read a YAML config file."""
  65.     logger.debug('Reading config file: %s' % config_file)
  66.     config_dir = os.path.dirname(config_file)
  67.     base_config = {}
  68.     with open(config_file) as fh:
  69.         data = yaml.load(fh)
  70.  
  71.     if data.get('extends'):
  72.         parent_config = os.path.join(config_dir, data['extends'])
  73.         base_config = read_config(parent_config)
  74.  
  75.     return dict_update(base_config, data)
  76.  
  77. def render_templates(template_dir, files, context):
  78.     """Render templates from `template_dir`."""
  79.     env = Environment(loader=FileSystemLoader(template_dir))
  80.     logger.debug('Jinja environment configured for: %s' % template_dir)
  81.  
  82.     for src, dest in files.items():
  83.         dir_name = os.path.dirname(dest)
  84.         if not os.path.exists(dir_name):
  85.             logger.debug('Creating directory %s' % dir_name)
  86.             os.makedirs(dir_name)
  87.         if src.endswith(('tpl', 'conf')):
  88.             logger.info('Writing %s -> %s' % (src, dest))
  89.             template = env.get_template(src)
  90.             with open(dest, 'w') as fh:
  91.                 fh.write(template.render(**context).encode('utf-8'))
  92.         else:
  93.             logger.info('Copying %s -> %s' % (src, dest))
  94.             shutil.copy(os.path.join(template_dir, src), dest)
  95.  
  96. def munge_context(variables, colors):
  97.     context = {}
  98.     context.update(variables)
  99.  
  100.     # Handle case when a variable may reference a color, e.g.
  101.     # `primary` = `alt_red`, then `primary` = `#fd1a2b`
  102.     for key, value in context.items():
  103.         if value in colors:
  104.             context[key] = colors[value]
  105.     context.update(colors)
  106.  
  107.     for key, value in DEFAULT_CONTEXT_CONFIG.items():
  108.         if key not in context:
  109.             context[key] = context[value]
  110.  
  111.     return context
  112.  
  113. def wallfix(directory, colors):
  114.     """Look in `directory` for file named `wallpaper.xxx` and set it."""
  115.     wallpaper = None
  116.     for filename in os.listdir(directory):
  117.         if filename.startswith('wallpaper'):
  118.             wallpaper = filename
  119.             break
  120.  
  121.     if not wallpaper:
  122.         logger.info('No wallpaper found, generating new one.')
  123.         wallpaper = create_wallpaper(colors, directory)
  124.  
  125.     logger.info('Setting %s as wallpaper' % wallpaper)
  126.     path = os.path.join(directory, wallpaper)
  127.     os.system('wallfix %s' % path)
  128.  
  129. def hex_to_rgb(h):
  130.     h = h.lstrip('#')
  131.     return tuple(map(lambda n: int(n, 16), [h[i:i+2] for i in range(0, 6, 2)]))
  132.  
  133. def rgb_to_hex(rgb):
  134.     return '#%s' % ''.join(('%02x' % p for p in rgb))
  135.  
  136. def create_wallpaper(colors, template_dir, w=1920, h=1200, filename='wallpaper.png'):
  137.     rectangles = (
  138.         # x1, y1, x2, y2 -- in percents
  139.         ('red', [0, 30.0, 3.125, 72.5]), # LEFT
  140.         ('green', [50, 0, 76.5625, 12.5]), # TOP
  141.         ('yellow', [96.875, 30.0, 100, 72.5]), # RIGHT
  142.         ('magenta', [23.4375, 25.0, 50, 30.0]), # MID TOP LEFT
  143.         ('white', [23.4375, 30.0, 50, 72.5]), # MID LEFT
  144.         ('magenta', [50, 30.0, 76.5625, 72.5]), # MID RIGHT
  145.         ('white', [50, 72.5, 76.5625, 87.5]), # MID BOTTOM RIGHT
  146.     )
  147.     def fix_coords(coords):
  148.         m = [w, h, w, h]
  149.         return [int(c * .01 * m[i]) for i, c in enumerate(coords)]
  150.     background = hex_to_rgb(colors['black'])
  151.     image = Image.new('RGB', (w, h), background)
  152.     draw = ImageDraw.Draw(image)
  153.     for color, coords in rectangles:
  154.         x1, y1, x2, y2 = fix_coords(coords)
  155.         draw.rectangle([(x1, y1), (x2, y2)], fill=hex_to_rgb(colors[color]))
  156.     image.save(os.path.join(template_dir, filename), 'PNG')
  157.     return filename
  158.  
  159. Point = namedtuple('Point', ('coords', 'ct'))
  160. Cluster = namedtuple('Cluster', ('points', 'center'))
  161.  
  162. def symlink(theme_name):
  163.     """Set up a symlink for the new theme."""
  164.     logger.info('Setting %s as current theme' % theme_name)
  165.     current = os.path.join(THEMER_ROOT, 'current')
  166.     if os.path.islink(current):
  167.         os.unlink(current)
  168.     os.symlink(os.path.join(THEMER_ROOT, theme_name), current)
  169.  
  170. def activate(theme_name):
  171.     """Activate the given theme."""
  172.     symlink(theme_name)
  173.     dest = os.path.join(THEMER_ROOT, theme_name)
  174.     color_file = os.path.join(dest, 'colors.yaml')
  175.     colors = CachedColorParser(color_file).read()
  176.     wallfix(dest, colors)
  177.     IconUpdater(colors['primary'], colors['secondary']).update_icons()
  178.     os.system('xrdb -merge ~/.Xresources')
  179.     os.system('i3-msg -q restart')
  180.  
  181. def fetch_vim(color_file):
  182.     return urllib2.urlopen('http://sweyla.com/themes/vim/sweyla%s.vim' % color_file).read()
  183.  
  184. def generate(color_source, config_file, template_dir, theme_name):
  185.     """Generate a new theme."""
  186.     destination = os.path.join(THEMER_ROOT, theme_name)
  187.     wallpaper = None
  188.     if color_source.isdigit() and not os.path.isfile(color_source):
  189.         colors = SweylaColorParser(color_source).read()
  190.         vim = fetch_vim(color_source)
  191.     elif color_source.lower().endswith(('.jpg', '.png', '.jpeg')):
  192.         colors = AutodetectColorParser(color_source).read()
  193.         wallpaper = color_source
  194.         vim = None
  195.     else:
  196.         colors = ColorParser(color_source).read()
  197.         vim = None
  198.     config = read_config(config_file)
  199.     context = munge_context(config['variables'], colors)
  200.     files = {
  201.         key: os.path.join(destination, value)
  202.         for key, value in config['files'].items()}
  203.     if wallpaper:
  204.         # Add wallpaper to the list of files to copy.
  205.         files[wallpaper] = os.path.join(
  206.             destination,
  207.             'wallpaper%s' % os.path.splitext(wallpaper)[1])
  208.  
  209.     render_templates(template_dir, files, context)
  210.  
  211.     # Save a copy of the colors in the generated theme folder.
  212.     with open(os.path.join(destination, 'colors.yaml'), 'w') as fh:
  213.         yaml.dump(context, fh, default_flow_style=False)
  214.  
  215.     # Save the vim color scheme.
  216.     if vim:
  217.         logger.info('Saving vim colorscheme %s.vim' % theme_name)
  218.         filename = os.path.join(os.environ['HOME'], '.vim/colors/%s.vim' % theme_name)
  219.         with open(filename, 'w') as fh:
  220.             fh.write(vim)
  221.  
  222.  
  223. class ColorParser(object):
  224.     # Colors look something like "*color0:  #FF0d3c\n"
  225.     color_re = re.compile('.*?(color[^:]+|background|foreground):\s*(#[\da-z]{6})')
  226.  
  227.     def __init__(self, color_file):
  228.         self.color_file = color_file
  229.         self.colors = {}
  230.  
  231.     def mapping(self):
  232.         return {
  233.             'background': 'background',
  234.             'foreground': 'foreground',
  235.             'color0': 'black',
  236.             'color8': 'alt_black',
  237.             'color1': 'red',
  238.             'color9': 'alt_red',
  239.             'color2': 'green',
  240.             'color10': 'alt_green',
  241.             'color3': 'yellow',
  242.             'color11': 'alt_yellow',
  243.             'color4': 'blue',
  244.             'color12': 'alt_blue',
  245.             'color5': 'magenta',
  246.             'color13': 'alt_magenta',
  247.             'color6': 'cyan',
  248.             'color14': 'alt_cyan',
  249.             'color7': 'white',
  250.             'color15': 'alt_white',
  251.             'colorul': 'underline'}
  252.  
  253.     def read(self):
  254.         color_mapping = self.mapping()
  255.  
  256.         with open(self.color_file) as fh:
  257.             for line in fh.readlines():
  258.                 if line.startswith('!'):
  259.                     continue
  260.                 match_obj = self.color_re.search(line.lower())
  261.                 if match_obj:
  262.                     var, color = match_obj.groups()
  263.                     self.colors[color_mapping[var]] = color
  264.  
  265.         if len(self.colors) < 16:
  266.             logger.warning(
  267.                 'Error, only %s colors were read when loading color file "%s"'
  268.                 % (len(self.colors), self.color_file))
  269.         return self.colors
  270.  
  271.  
  272. class SweylaColorParser(ColorParser):
  273.     def mapping(self):
  274.         return {
  275.             'bg': ['background', 'black', 'alt_black'],
  276.             'fg': ['foreground', 'white'],
  277.             'nf': 'red',  # name of function / method
  278.             'nd': 'alt_red',  # decorator
  279.             'nc': 'green',  # name of class
  280.             'nt': 'alt_green', # ???
  281.             'nb': 'yellow',  # e.g., "object" or "open"
  282.             'c': 'alt_yellow',  # comments
  283.             's': 'blue',  # string
  284.             'mi': 'alt_blue',  # e.g., a number
  285.             'k': 'magenta',  # e.g., "class"
  286.             'o': 'alt_magenta', # operator, e.g "="
  287.             'bp': 'cyan',  # e.g., "self" keyword
  288.             'si': 'alt_cyan', # e.g. "%d"
  289.             'se': 'alt_white',
  290.             'support_function': 'underline'}
  291.  
  292.     def read(self):
  293.         mapping = self.mapping()
  294.         resp = urllib2.urlopen(
  295.             'http://sweyla.com/themes/textfile/sweyla%s.txt' % self.color_file)
  296.         contents = resp.read()
  297.         for line in contents.splitlines():
  298.             key, value = line.split(':\t')
  299.             if key in mapping:
  300.                 colors = mapping[key]
  301.                 if not isinstance(colors, list):
  302.                     colors = [colors]
  303.                 for color in colors:
  304.                     self.colors[color] = value
  305.         return self.colors
  306.  
  307. class AutodetectColorParser(ColorParser):
  308.     def __init__(self, wallpaper_file, k=16, bg='#0e0e0e', fg='#ffffff'):
  309.         self.wallpaper_file = wallpaper_file
  310.         self.k = k
  311.         self.bg = bg
  312.         self.fg = fg
  313.  
  314.     def _get_points_from_image(self, img):
  315.         points = []
  316.         w, h = img.size
  317.         for count, color in img.getcolors(w * h):
  318.             points.append(Point(color, count))
  319.         return points
  320.  
  321.     def get_dominant_colors(self):
  322.         img = Image.open(self.wallpaper_file)
  323.         img.thumbnail((300, 300))  # Resize to speed up python loop.
  324.         width, height = img.size
  325.         points = self._get_points_from_image(img)
  326.         clusters = self.kmeans(points, self.k, 1)
  327.         rgbs = [map(int, c.center.coords) for c in clusters]
  328.         return map(rgb_to_hex, rgbs)
  329.  
  330.     def _euclidean_dist(self, p1, p2):
  331.         return math.sqrt(
  332.             sum((p1.coords[i] - p2.coords[i]) ** 2 for i in range(3)))
  333.  
  334.     def _calculate_center(self, points):
  335.         vals = [0.0 for i in range(3)]
  336.         plen = 0
  337.         for p in points:
  338.             plen += p.ct
  339.             for i in range(3):
  340.                 vals[i] += (p.coords[i] * p.ct)
  341.         return Point([(v / plen) for v in vals], 1)
  342.  
  343.     def kmeans(self, points, k, min_diff):
  344.         clusters = [Cluster([p], p) for p in random.sample(points, k)]
  345.         logger.info('Calculating %d dominant colors.' % k)
  346.         while True:
  347.             plists = [[] for i in range(k)]
  348.             for p in points:
  349.                 smallest_distance = float('Inf')
  350.                 for i in range(k):
  351.                     distance = self._euclidean_dist(p, clusters[i].center)
  352.                     if distance < smallest_distance:
  353.                         smallest_distance = distance
  354.                         idx = i
  355.                 plists[idx].append(p)
  356.             diff = 0
  357.             for i in range(k):
  358.                 old = clusters[i]
  359.                 center = self._calculate_center(plists[i])
  360.                 new = Cluster(plists[i], center)
  361.                 clusters[i] = new
  362.                 diff = max(diff, self._euclidean_dist(old.center, new.center))
  363.             logger.debug('Diff: %d' % diff)
  364.             if diff <= min_diff:
  365.                 break
  366.         return clusters
  367.  
  368.     def normalize(self, hexv, minv=128, maxv=256):
  369.         r, g, b = hex_to_rgb(hexv)
  370.         h, s, v = colorsys.rgb_to_hsv(r / 256.0, g / 256.0, b / 256.0)
  371.         minv = minv / 256.0
  372.         maxv = maxv / 256.0
  373.         if v < minv:
  374.             v = minv
  375.         if v > maxv:
  376.             v = maxv
  377.         rgb = colorsys.hsv_to_rgb(h, s, v)
  378.         return rgb_to_hex(map(lambda i: i * 256, rgb))
  379.  
  380.     def read(self):
  381.         colors = self.get_dominant_colors()
  382.         color_dict = {
  383.             'background': self.bg,
  384.             'foreground': self.fg}
  385.         for i, color in enumerate(itertools.cycle(colors)):
  386.             if i == 0:
  387.                 color = self.normalize(color, minv=0, maxv=32)
  388.             elif i == 8:
  389.                 color = self.normalize(color, minv=128, maxv=192)
  390.             elif i < 8:
  391.                 color = self.normalize(color, minv=160, maxv=224)
  392.             else:
  393.                 color = self.normalize(color, minv=200, maxv=256)
  394.             color_dict['color%d' % i] = color
  395.             if i == 15:
  396.                 break
  397.         mapping = self.mapping()
  398.         translated = {}
  399.         for k, v in color_dict.items():
  400.             translated[mapping[k]] = v
  401.         logger.debug(translated)
  402.         return translated
  403.  
  404. class CachedColorParser(ColorParser):
  405.     def read(self):
  406.         with open(self.color_file) as fh:
  407.             self.colors = yaml.load(fh)
  408.         return self.colors
  409.  
  410.  
  411. class IconUpdater(object):
  412.     def __init__(self, primary_color, secondary_color):
  413.         self.primary_color = primary_color
  414.         self.secondary_color = secondary_color
  415.  
  416.     def icon_path(self):
  417.         return os.path.join(os.environ['HOME'], '.icons/acyl')
  418.  
  419.     def primary_icon(self):
  420.         return os.path.join(self.icon_path(), 'scalable/places/desktop.svg')
  421.  
  422.     def secondary_icon(self):
  423.         return os.path.join(self.icon_path(), 'scalable/actions/add.svg')
  424.  
  425.     def extract_color_svg(self, filename):
  426.         regex = re.compile('stop-color:(#[\da-zA-Z]{6})')
  427.         with open(filename, 'r') as fh:
  428.             for line in fh.readlines():
  429.                 match_obj = regex.search(line)
  430.                 if match_obj:
  431.                     return match_obj.groups()[0]
  432.         raise ValueError('Unable to determine icon color.')
  433.  
  434.     def update_icons(self):
  435.         # Introspect a couple icon files to determine what colors are being used
  436.         # currently.
  437.         old_primary = self.extract_color_svg(self.primary_icon())
  438.         old_secondary = self.extract_color_svg(self.secondary_icon())
  439.         logger.debug('Old icon colors: %s, %s' % (old_primary, old_secondary))
  440.  
  441.         # Walk the icons, updating the colors in each svg file.
  442.         file_count = 0
  443.         for root, dirs, filenames in os.walk(self.icon_path()):
  444.             for filename in filenames:
  445.                 if not filename.endswith('.svg'):
  446.                     continue
  447.                 path = os.path.join(root, filename)
  448.                 with open(path, 'r+') as fh:
  449.                     contents = fh.read()
  450.                     contents = contents.replace(old_primary, self.primary_color)
  451.                     contents = contents.replace(old_secondary, self.secondary_color)
  452.                     fh.seek(0)
  453.                     fh.write(contents)
  454.                     file_count += 1
  455.         logger.info('Checked %d icon files' % file_count)
  456.  
  457.  
  458. def get_parser():
  459.     parser = optparse.OptionParser(usage='usage: %prog [options] [list|activate|generate|current|delete] theme_name [color file]')
  460.     parser.add_option('-t', '--template', dest='template_dir', default='i3')
  461.     parser.add_option('-c', '--config', dest='config_file', default='config.yaml')
  462.     parser.add_option('-a', '--activate', dest='activate', action='store_true')
  463.     parser.add_option('-v', '--verbose', dest='verbose', action='store_true')
  464.     parser.add_option('-d', '--debug', dest='debug', action='store_true')
  465.     return parser
  466.  
  467. def panic(msg):
  468.     print >> sys.stderr, msg
  469.     sys.exit(1)
  470.  
  471. if __name__ == '__main__':
  472.     parser = get_parser()
  473.     options, args = parser.parse_args()
  474.  
  475.     if not args:
  476.         panic(parser.get_usage())
  477.  
  478.     action = args[0]
  479.     if action not in ('list', 'activate', 'generate', 'current', 'delete'):
  480.         panic('Unknown action "%s"' % action)
  481.  
  482.     if action not in ('list', 'current') and len(args) == 1:
  483.         panic('Missing required argument "theme_name"')
  484.     elif action == 'list':
  485.         themes = [
  486.             t for t in os.listdir(THEMER_ROOT)
  487.             if t not in ('templates', 'current')]
  488.         print '\n'.join(sorted(themes))
  489.         sys.exit(0)
  490.     elif action == 'current':
  491.         current = os.path.join(THEMER_ROOT, 'current')
  492.         if not os.path.exists(current):
  493.             print 'No theme'
  494.         else:
  495.             print os.path.basename(os.path.realpath(
  496.                 os.path.join(THEMER_ROOT, 'current')))
  497.             os.system('colortheme')
  498.         sys.exit(0)
  499.  
  500.     theme_name = args[1]
  501.  
  502.     # Add logging handlers.
  503.     if options.verbose or options.debug:
  504.         handler = logging.StreamHandler()
  505.         logger.addHandler(handler)
  506.     if options.debug:
  507.         logger.setLevel(logging.DEBUG)
  508.  
  509.     if action == 'activate':
  510.         activate(theme_name)
  511.     elif action == 'delete':
  512.         shutil.rmtree(os.path.join(THEMER_ROOT, theme_name))
  513.         logger.info('Removed %s' % theme_name)
  514.     else:
  515.         # Find the appropriate yaml config file and load it.
  516.         template_dir = os.path.join(TEMPLATE_ROOT, options.template_dir)
  517.         config_file = os.path.join(template_dir, options.config_file)
  518.         if not os.path.exists(config_file):
  519.             panic('Unable to find file "%s"' % config_file)
  520.  
  521.         if not len(args) == 3:
  522.             panic('Missing required color file')
  523.         else:
  524.             color_file = args[2]
  525.  
  526.         generate(color_file, config_file, template_dir, theme_name)
  527.  
  528.         if options.activate or raw_input('Activate now? yN ') == 'y':
  529.             activate(theme_name)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement