├── requirements.txt ├── setup.cfg ├── colorz.png ├── .gitignore ├── LICENSE ├── setup.py ├── README.rst └── colorz.py /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | scipy 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | -------------------------------------------------------------------------------- /colorz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metakirby5/colorz/HEAD/colorz.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ethan Chan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | 3 | from setuptools import setup 4 | from os import path 5 | 6 | here = path.abspath(path.dirname(__file__)) 7 | 8 | # Get the long description from the README file 9 | with codecs.open(path.join(here, 'README.rst'), encoding='utf-8') as f: 10 | long_description = f.read() 11 | 12 | setup( 13 | name='colorz', 14 | version='1.0.3', 15 | description='Color scheme generator.', 16 | long_description=long_description, 17 | url='https://github.com/metakirby5/colorz', 18 | author='Ethan Chan', 19 | author_email='metakirby5@gmail.com', 20 | license='MIT', 21 | classifiers=[ 22 | 'Development Status :: 5 - Production/Stable', 23 | 'Environment :: Console', 24 | 'Intended Audience :: End Users/Desktop', 25 | 'Topic :: Utilities', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Natural Language :: English', 28 | 'Operating System :: POSIX :: Linux', 29 | 30 | 'Programming Language :: Python :: 2', 31 | 'Programming Language :: Python :: 3', 32 | ], 33 | keywords='colorz color scheme theme generator', 34 | py_modules=['colorz'], 35 | install_requires=[ 36 | 'Pillow', 37 | 'scipy', 38 | ], 39 | entry_points={ 40 | 'console_scripts': [ 41 | 'colorz=colorz:main', 42 | ], 43 | }, 44 | ) 45 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | colorz 3 | ======== 4 | 5 | |Sample Usage| 6 | 7 | A k-means color scheme generator. 8 | 9 | Installation 10 | ------------ 11 | 12 | :: 13 | 14 | pip install colorz 15 | 16 | or just move ``colorz.py`` to somewhere in your ``$PATH``. 17 | If you do the latter, you must install the dependencies in the 18 | following section manually. 19 | 20 | Arch Linux: 21 | 22 | :: 23 | 24 | git clone https://aur.archlinux.org/colorz.git 25 | cd colorz 26 | makepkg -si 27 | 28 | Dependencies 29 | ------------ 30 | 31 | - Python (2 or 3) 32 | - Pillow 33 | - scipy 34 | 35 | Usage 36 | ----- 37 | 38 | :: 39 | 40 | usage: colorz [-h] [-n NUM_COLORS] [--minv MINV] [--maxv MAXV] [--bold BOLD] 41 | [--font-size FONT_SIZE] [--bg-color BG_COLOR] [--no-bg-img] 42 | [--no-preview] 43 | image 44 | 45 | A color scheme generator. Takes an image (local or online) and grabs the most 46 | dominant colors using kmeans. Also creates bold colors by adding value to the 47 | dominant colors. Finally, outputs the colors to stdout (one normal and one 48 | bold per line, space delimited) and generates an HTML preview of the color 49 | scheme. 50 | 51 | positional arguments: 52 | image the image file or url to generate from. 53 | 54 | optional arguments: 55 | -h, --help show this help message and exit 56 | -n NUM_COLORS number of colors to generate (excluding bold). 57 | Default: 6 58 | --minv MINV minimum value for the colors. Default: 170 59 | --maxv MAXV maximum value for the colors. Default: 200 60 | --bold BOLD how much value to add for bold colors. Default: 50 61 | --font-size FONT_SIZE 62 | what font size to use, in rem. Default: 1 63 | --bg-color BG_COLOR what background color to use, in hex format. Default: 64 | #272727 65 | --no-bg-img whether or not to use a background image in the 66 | preview. Default: background image on 67 | --no-preview whether or not to generate and show the preview. 68 | Default: preview on 69 | 70 | Thanks to 71 | --------- 72 | 73 | - http://charlesleifer.com/blog/using-python-and-k-means-to-find-the-dominant-colors-in-images/ 74 | - https://gist.github.com/radiosilence/3946121 75 | 76 | .. |Sample Usage| image:: http://i.imgur.com/QVLSXqK.png 77 | :target: colorz.png 78 | :alt: Color preview. 79 | -------------------------------------------------------------------------------- /colorz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | A color scheme generator. 6 | 7 | Takes an image (local or online) and grabs the most dominant colors 8 | using kmeans. 9 | Also creates bold colors by adding value to the dominant colors. 10 | 11 | Finally, outputs the colors to stdout 12 | (one normal and one bold per line, space delimited) and 13 | generates an HTML preview of the color scheme. 14 | """ 15 | 16 | import os 17 | import webbrowser 18 | from sys import exit 19 | from io import BytesIO 20 | from tempfile import NamedTemporaryFile 21 | from argparse import ArgumentParser 22 | from PIL import Image 23 | from numpy import array 24 | from scipy.cluster.vq import kmeans 25 | from colorsys import rgb_to_hsv, hsv_to_rgb 26 | 27 | # Python3 compatibility 28 | try: 29 | from urllib2 import urlopen 30 | except ImportError: 31 | from urllib.request import urlopen 32 | 33 | DEFAULT_NUM_COLORS = 6 34 | DEFAULT_MINV = 170 35 | DEFAULT_MAXV = 200 36 | DEFAULT_BOLD_ADD = 50 37 | DEFAULT_FONT_SIZE = 1 38 | DEFAULT_BG_COLOR = '#272727' 39 | 40 | THUMB_SIZE = (200, 200) 41 | SCALE = 256.0 42 | 43 | 44 | def down_scale(x): 45 | return x / SCALE 46 | 47 | 48 | def up_scale(x): 49 | return int(x * SCALE) 50 | 51 | 52 | def hexify(rgb): 53 | return '#%s' % ''.join('%02x' % p for p in rgb) 54 | 55 | 56 | def get_colors(img): 57 | """ 58 | Returns a list of all the image's colors. 59 | """ 60 | w, h = img.size 61 | return [color[:3] for count, color in img.convert('RGB').getcolors(w * h)] 62 | 63 | 64 | def clamp(color, min_v, max_v): 65 | """ 66 | Clamps a color such that the value is between min_v and max_v. 67 | """ 68 | h, s, v = rgb_to_hsv(*map(down_scale, color)) 69 | min_v, max_v = map(down_scale, (min_v, max_v)) 70 | v = min(max(min_v, v), max_v) 71 | return tuple(map(up_scale, hsv_to_rgb(h, s, v))) 72 | 73 | 74 | def order_by_hue(colors): 75 | """ 76 | Orders colors by hue. 77 | """ 78 | hsvs = [rgb_to_hsv(*map(down_scale, color)) for color in colors] 79 | hsvs.sort(key=lambda t: t[0]) 80 | return [tuple(map(up_scale, hsv_to_rgb(*hsv))) for hsv in hsvs] 81 | 82 | 83 | def brighten(color, brightness): 84 | """ 85 | Adds or subtracts value to a color. 86 | """ 87 | h, s, v = rgb_to_hsv(*map(down_scale, color)) 88 | return tuple(map(up_scale, hsv_to_rgb(h, s, v + down_scale(brightness)))) 89 | 90 | 91 | def colorz(fd, n=DEFAULT_NUM_COLORS, min_v=DEFAULT_MINV, max_v=DEFAULT_MAXV, 92 | bold_add=DEFAULT_BOLD_ADD, order_colors=True): 93 | """ 94 | Get the n most dominant colors of an image. 95 | Clamps value to between min_v and max_v. 96 | 97 | Creates bold colors using bold_add. 98 | Total number of colors returned is 2*n, optionally ordered by hue. 99 | Returns as a list of pairs of RGB triples. 100 | 101 | For terminal colors, the hue order is: 102 | red, yellow, green, cyan, blue, magenta 103 | """ 104 | img = Image.open(fd) 105 | img.thumbnail(THUMB_SIZE) 106 | 107 | obs = get_colors(img) 108 | clamped = [clamp(color, min_v, max_v) for color in obs] 109 | clusters, _ = kmeans(array(clamped).astype(float), n) 110 | colors = order_by_hue(clusters) if order_colors else clusters 111 | return list(zip(colors, [brighten(c, bold_add) for c in colors])) 112 | 113 | 114 | def html_preview(colors, font_size=DEFAULT_FONT_SIZE, 115 | bg_color=DEFAULT_BG_COLOR, bg_img=None, 116 | fd=None): 117 | """ 118 | Creates an HTML preview of each color. 119 | 120 | Returns the Python file object for the HTML file. 121 | """ 122 | 123 | fd = fd or NamedTemporaryFile(mode='wt', suffix='.html', delete=False) 124 | 125 | # Initial CSS styling is empty 126 | style = "" 127 | 128 | # Create the main body 129 | body = '\n'.join([""" 130 |
131 |
█ {color}
132 |
133 | █ {color_bold} 134 |
135 |
136 | """.format(color=hexify(c[0]), color_bold=hexify(c[1])) for c in colors]) 137 | 138 | if bg_img: 139 | # Check if local or online image 140 | if os.path.isfile(bg_img): 141 | bg_img = os.path.abspath(bg_img) 142 | 143 | bg_url = "url('%s')" % ( 144 | ('file://%s' % bg_img) if os.path.isfile(bg_img) else bg_img) 145 | 146 | # Add preview box and image to the body 147 | body = """ 148 |
149 | 150 | {body} 151 |
152 | """.format(**locals()) 153 | 154 | # Add blurred background image styling 155 | style += """ 156 | body:before {{ 157 | content: ''; 158 | position: fixed; 159 | z-index: -1; 160 | left: 0; 161 | right: 0; 162 | width: 100%; 163 | height: 100%; 164 | display: block; 165 | 166 | background-image: {bg_url}; 167 | background-size: cover; 168 | background-repeat: no-repeat; 169 | background-position: center center; 170 | background-attachment: fixed; 171 | 172 | -webkit-filter: blur(2rem); 173 | -moz-filter: blur(2rem); 174 | -o-filter: blur(2rem); 175 | -ms-filter: blur(2rem); 176 | filter: blur(2rem) 177 | }} 178 | """.format(**locals()) 179 | 180 | # CSS styling 181 | style += """ 182 | body {{ 183 | margin: 0; 184 | background: {bg_color}; 185 | 186 | font-family: monospace; 187 | font-size: {font_size}rem; 188 | line-height: 1; 189 | }} 190 | 191 | #main-container {{ 192 | padding: 1rem; 193 | text-align: center; 194 | }} 195 | 196 | #preview-box {{ 197 | display: inline-block; 198 | margin: 3rem; 199 | padding: 1rem; 200 | background: {bg_color}; 201 | }} 202 | 203 | #preview-image {{ 204 | width: 100%; 205 | }} 206 | 207 | .color {{ 208 | display: inline-block; 209 | margin: 1rem; 210 | }} 211 | 212 | .box-shadow {{ 213 | -webkit-box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.75); 214 | -moz-box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.75); 215 | box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.75); 216 | }} 217 | """.format(**locals()) 218 | 219 | # Write the file 220 | fd.write(""" 221 | 222 | 223 | 224 | 225 | Colorscheme Preview 226 | 227 | 228 | 231 | 232 | 233 |
234 | {body} 235 |
236 | 237 | 238 | """.format(**locals())) 239 | 240 | return fd 241 | 242 | 243 | def parse_args(): 244 | parser = ArgumentParser(description=__doc__) 245 | 246 | parser.add_argument('image', 247 | help=""" 248 | the image file or url to generate from. 249 | """, 250 | type=str) 251 | 252 | parser.add_argument('-n', 253 | help=""" 254 | number of colors to generate (excluding bold). 255 | Default: %s 256 | """ % DEFAULT_NUM_COLORS, 257 | dest='num_colors', 258 | type=int, 259 | default=DEFAULT_NUM_COLORS) 260 | 261 | parser.add_argument('--minv', 262 | help=""" 263 | minimum value for the colors. 264 | Default: %s 265 | """ % DEFAULT_MINV, 266 | type=int, 267 | default=DEFAULT_MINV) 268 | 269 | parser.add_argument('--maxv', 270 | help=""" 271 | maximum value for the colors. 272 | Default: %s 273 | """ % DEFAULT_MAXV, 274 | type=int, 275 | default=DEFAULT_MAXV) 276 | 277 | parser.add_argument('--bold', 278 | help=""" 279 | how much value to add for bold colors. 280 | Default: %s 281 | """ % DEFAULT_BOLD_ADD, 282 | type=int, 283 | default=DEFAULT_BOLD_ADD) 284 | 285 | parser.add_argument('--font-size', 286 | help=""" 287 | what font size to use, in rem. 288 | Default: %s 289 | """ % DEFAULT_FONT_SIZE, 290 | type=int, 291 | default=DEFAULT_FONT_SIZE) 292 | 293 | parser.add_argument('--bg-color', 294 | help=""" 295 | what background color to use, in hex format. 296 | Default: %s 297 | """ % DEFAULT_BG_COLOR, 298 | type=str, 299 | default=DEFAULT_BG_COLOR) 300 | 301 | parser.add_argument('--no-bg-img', 302 | help=""" 303 | whether or not to use a background image in the 304 | preview. 305 | Default: background image on 306 | """, 307 | action='store_true', 308 | default=False) 309 | 310 | parser.add_argument('--no-preview', 311 | help=""" 312 | whether or not to generate and show the preview. 313 | Default: preview on 314 | """, 315 | action='store_true', 316 | default=False) 317 | 318 | return parser.parse_args() 319 | 320 | 321 | def main(): 322 | args = parse_args() 323 | 324 | # Open local file or online file 325 | try: 326 | img_fd = open(args.image, 'rb') if os.path.isfile(args.image) else \ 327 | BytesIO(urlopen(args.image).read()) 328 | except ValueError: 329 | print("%s was not a valid URL." % args.image) 330 | exit(1) 331 | 332 | colors = colorz(img_fd, args.num_colors, args.minv, args.maxv, args.bold) 333 | 334 | for pair in colors: 335 | print('%s %s' % tuple(map(hexify, pair))) 336 | 337 | if not args.no_preview: 338 | html_fd = html_preview(colors, args.font_size, args.bg_color, 339 | args.image if not args.no_bg_img else None) 340 | 341 | # Suppress stdout from browser 342 | # http://stackoverflow.com/a/2323563 343 | os.close(1) 344 | os.close(2) 345 | webbrowser.open('file://%s' % html_fd.name) 346 | 347 | 348 | if __name__ == '__main__': 349 | main() 350 | --------------------------------------------------------------------------------