#!/usr/bin/env python # Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # coding=utf-8 import argparse import copy import json from math import sqrt import re import urllib2 import sys from os.path import basename, splitext COLORS_JSON_PREFIX = splitext(basename(__file__))[0] COLORLOVERS_COLORS_JSON = COLORS_JSON_PREFIX + "_colorlovers.json" WIKIPEDIA_COLORS_JSON = COLORS_JSON_PREFIX + "_wikipedia.json" def download_values_from_color_lovers(r, g, b): """ Returns values for a single color from colourlovers.com NOTE: does a single HTTP request per call, please call wisely """ url = "http://www.colourlovers.com/api/color/%02x%02x%02x?format=json" % (r, g, b) opener = urllib2.build_opener() opener.addheaders = [('User-agent', 'Mozilla/5.0')] response = opener.open(url) s = response.read() values = json.loads(s) print "r: %03d, g:%03d, b:%03d: %s" % (r, g, b, values) return values def download_all_colors_from_color_lovers(): """ Requests and caches into colorlovers_colors.json all 64 colors """ colors = [] for r2 in range(0, 4): for g2 in range(0, 4): for b2 in range(0, 4): colors += download_values_from_color_lovers(r2*85, g2*85, b2*85) with open(COLORLOVERS_COLORS_JSON, "w") as f: json.dump({"colors": colors}, f, indent=2) def load_cached_colorlovers_colors(): """ Loads cached color values from colourlovers.com and converts them to the expected format """ with open(COLORLOVERS_COLORS_JSON) as f: colors = json.load(f)["colors"] return [{"r": c["rgb"]["red"], "g": c["rgb"]["green"], "b": c["rgb"]["blue"], "name": c["title"], "source": c["url"]} for c in colors] def parse_colors_from_wikipedia_html(html): """ Requests and return all color values from a single, paginated wikipedia "list of colors" page """ from bs4 import BeautifulSoup url_base = "http://en.wikipedia.org" colors = [] soup = BeautifulSoup(html) for tr in soup.find("table").find_all("tr"): tds = tr.find_all("td") if len(tds) == 9: hex = tds[0].text.strip() r = int(hex[1:1+2], 16) g = int(hex[3:3+2], 16) b = int(hex[5:5+2], 16) color = {"r": r, "g": g, "b": b} th_a = tr.find("th").find("a") if th_a is None: color["name"] = tr.find("th").text.strip() color["url"] = "http://en.wikipedia.org/wiki/List_of_colors" else: color["name"] = th_a.text.strip() color["url"] = url_base + th_a["href"] print color colors.append(color) return colors def download_and_parse_colors_from_wikipedia(): """ Requests and caches into wikipedia_colors.json all available colors from wikipedias "list of colors" pages """ import wikipedia colors = [] for title in ["List of colors: A-F", "List of colors: G-M", "List of colors: N-Z"]: colors += parse_colors_from_wikipedia_html(wikipedia.page(title).html()) with open(WIKIPEDIA_COLORS_JSON, "w") as f: json.dump({"colors": colors}, f, indent=2) return colors def load_cached_wikipedia_colors(): """ loads all previously cached colors from wikipedia """ with open(WIKIPEDIA_COLORS_JSON) as f: return [copy.copy(c) for c in json.load(f)["colors"]] def hardwired_colors(): """ creates a set of hard-wired colors. Used to overrule any other source. """ result = [] result.append({'r': 0, 'g': 85, 'b': 255, 'name': u'Blue Moon', 'url': 'http://en.wikipedia.org/wiki/Blue_Moon_(beer)'}) result.append({'r': 0, 'g': 170, 'b': 85, 'name': u'Jaeger Green', 'url': 'http://en.wikipedia.org/wiki/Jägermeister'}) result.append({'r': 0, 'g': 255, 'b': 0, 'name': u'Green', 'url': 'http://en.wikipedia.org/wiki/Green'}) result.append({'r': 0, 'g': 255, 'b': 255, 'name': u'Cyan', 'url': 'http://en.wikipedia.org/wiki/Cyan'}) result.append({'r': 255, 'g': 0, 'b': 255, 'name': u'Magenta', 'url': 'http://en.wikipedia.org/wiki/Magenta'}) result.append({'r': 255, 'g': 0, 'b': 170, 'name': u'Fashion Magenta', 'url': 'http://en.wikipedia.org/wiki/Fuchsia_(color)#Fashion_fuchsia'}) result.append({'r': 255, 'g': 255, 'b': 0, 'name': u'Yellow', 'url': 'http://en.wikipedia.org/wiki/Yellow'}) result.append({'r': 255, 'g': 85, 'b': 0, 'name': u'Orange', 'url': 'http://en.wikipedia.org/wiki/Orange_(colour)'}) # verify with display result.append({'r': 170, 'g': 0, 'b': 170, 'name': u'Purple', 'url': 'http://en.wikipedia.org/wiki/Purple'}) # verify with display # TODO: find brown value, core graphics says: r:0.6,g:0.4,b:0.2 # colors to match CoreGraphics names result.append({'r': 85, 'g': 85, 'b': 85, 'name': u'Dark Gray', 'url': 'http://en.wikipedia.org/wiki/Shades_of_gray#Dark_medium_gray_.28dark_gray_.28X11.29.29'}) result.append({'r': 170, 'g': 170, 'b': 170, 'name': u'Light Gray', 'url': 'http://en.wikipedia.org/wiki/Shades_of_gray#Light_gray'}) return result def color_dist(a, b): # TODO: use YUV dist sum = 0 for c in ["r", "g", "b"]: sum += abs(a[c] - b[c]) ** 2 return sqrt(sum) def closest_color(c, colors): min_dist = None min_value = None for candidate in colors: dist = color_dist(c, candidate) if min_dist is None or dist < min_dist: min_dist = dist min_value = candidate if dist == 0: break return min_value def enhanced_color(color): """ Add additional, derived data to a color used for json output, c header file generation, etc. """ result = copy.copy(color) result["identifier"] = re.sub(r"\([^\)]+\)|[\s_'-]", " ", color["name"]).title().replace(" ", "") result["name"] = result["name"].title() r = result["r"] g = result["g"] b = result["b"] r2 = r / 85 g2 = g / 85 b2 = b / 85 c_identifier = "GColor%s" % result["identifier"] result["c_identifier"] = c_identifier result["c_value_identifier"] = "GColor%sARGB8" % result["identifier"] hex_value = "0x%0.2X%0.2X%0.2X" % (r, g, b) html_value = "#%0.2X%0.2X%0.2X" % (r, g, b) result["html"] = html_value binary = "0b11{0:02b}{1:02b}{2:02b}".format(r2, g2, b2) result["binary"] = binary result["literals"] = [ {"id": "define", "description": "SDK Constant", "value": c_identifier}, {"id": "rgb", "description": "Code (RGB)", "value": "GColorFromRGB(%d, %d, %d)" % (r, g, b)}, {"id": "hex", "description": "Code (Hex)", "value": "GColorFromHEX(%s)" % hex_value}, {"id": "html", "description": "HTML code", "value": html_value}, {"id": "gcolor_argb", "description": "GColor (argb)", "value": "(GColor){.argb=%s}" % binary}, {"id": "gcolor_fields", "description": "GColor (components)", "value": "(GColor){{.a=0b11, .r=0b{0:02b}, .g=0b{1:02b}, .b=0b{2:02b}}}".format(r2, g2, b2)}, ] return result def validate_colors(colors): """ Some sanity checks on the set of colors. """ if len(colors) != 64: raise Exception("Number of derived colors (%d) is different from expectation (64)", len(colors)) for c in colors: if len([cc for cc in colors if cc["identifier"] == c["identifier"]]) != 1: raise Exception("duplicate identifier name: %s and %s", c["name"]) def all_colors_with_names(): """ Will construct a set of our 64 colors with the closest colors from a hard-wired set of sources. """ candidates = [] candidates += hardwired_colors() try: candidates += load_cached_wikipedia_colors() # for now, we only look at colors from wikipedia # color lovers code can be deleted as soon as we agreed on final color names # candidates += load_colorlovers_colors() except IOError, e: raise IOError("%s\n\n%s" % (e, "make sure you called --download_wikipedia once")) result = [] for r2 in range(0, 4): for g2 in range(0, 4): for b2 in range(0, 4): c = {"r": r2 * 85, "g": g2 * 85, "b": b2 * 85} closest = closest_color(c, candidates) dist = color_dist(c, closest) c["dist"] = dist c["closest"] = closest for k in ["name", "url"]: if k in closest: c[k] = closest[k] result.append(enhanced_color(c)) validate_colors(result) return result def render_header(colors): """ produces the contents of color_definitions.h """ color_value_maxlen = max([len(c["c_value_identifier"]) for c in colors]) color_value_defines = [] color_value_defines.append("//%s AARRGGBB" % "".ljust(color_value_maxlen + len("#define (uint_8_t)"))) for c in colors: identifier = c["c_value_identifier"] color_value_defines.append( "#define %s ((uint8_t)%s)" % (identifier.ljust(color_value_maxlen), c["binary"]) ) color_define_maxlen = max([len(c["c_identifier"]) for c in colors]) color_defines = [] for c in colors: identifier = c["c_identifier"] value_identifier = c["c_value_identifier"] hex_value = "#%0.2X%0.2X%0.2X" % (c["r"], c["g"], c["b"]) color_defines.append("") color_defines.append( "//! %s" % (hex_value, hex_value, identifier)) color_defines.append( "#define %s (GColor8){.argb=%s}" % (identifier.ljust(color_define_maxlen), value_identifier) ) file_content = """#pragma once // @%s // THIS FILE HAS BEEN GENERATED, PLEASE DON'T MODIFY ITS CONTENT MANUALLY // USE /tools/%s TO MAKE CHANGES //! @addtogroup Graphics //! @{ //! @addtogroup GraphicsTypes //! @{ //! Convert RGBA to GColor. //! @param red Red value from 0 - 255 //! @param green Green value from 0 - 255 //! @param blue Blue value from 0 - 255 //! @param alpha Alpha value from 0 - 255 //! @return GColor created from the RGBA values #define GColorFromRGBA(red, green, blue, alpha) ((GColor8){ \\ .a = (uint8_t)(alpha) >> 6, \\ .r = (uint8_t)(red) >> 6, \\ .g = (uint8_t)(green) >> 6, \\ .b = (uint8_t)(blue) >> 6, \\ }) //! Convert RGB to GColor. //! @param red Red value from 0 - 255 //! @param green Green value from 0 - 255 //! @param blue Blue value from 0 - 255 //! @return GColor created from the RGB values #define GColorFromRGB(red, green, blue) \\ GColorFromRGBA(red, green, blue, 255) //! Convert hex integer to GColor. //! @param v Integer hex value (e.g. 0x64ff46) //! @return GColor created from the hex value #define GColorFromHEX(v) GColorFromRGB(((v) >> 16) & 0xff, ((v) >> 8) & 0xff, ((v) & 0xff)) //! @addtogroup ColorDefinitions Color Definitions //! A list of all of the named colors available with links to the color map on the Pebble Developer website. //! @{ // 8bit color values of all natively supported colors %s // GColor values of all natively supported colors %s // Additional 8bit color values #define GColorClearARGB8 ((uint8_t)0b00000000) // Additional GColor values #define GColorClear ((GColor8){.argb=GColorClearARGB8}) //! @} // group ColorDefinitions //! @} // group GraphicsTypes //! @} // group Graphics """ % ("generated", basename(__file__), "\n".join(color_value_defines), "\n".join(color_defines)) return file_content def render_html(colors): """ renders a HTML file for debugging purposes. Not even close to the awesome Pebble color picker(tm) """ html = '' html += '' html += "%s" % "".join(["" % s for s in ["r", "g", "b", "color", "color", "Δ", "r", "g", "b", "c code", "name", "identifier"]]) html += "" for c in colors: def rgb(c): return '' % (c["r"], c["g"], c["b"]) def color(c): return '' % (c["r"], c["g"], c["b"]) def c_code(c): return "(GColor){{.rgba=0b{:02b}{:02b}{:02b}11}}".format(c["r"] / 64, c["g"] / 64, c["b"] / 64) html += '' html += rgb(c["closest"]) html += color(c["closest"]) html += color(c) html += '' % c["dist"] html += rgb(c) html += '' html += '' % c["identifier"] html += "" html += "
ClosestActual Color
%s
%d%d%d
%d
'+c_code(c)+'
' if "url" in c: html += '%s' % (c["url"], c["name"]) else: html += c["name"] html += '%s
" return html def render_json(colors): """ JSON as being used by the awesome Pebble color picker(tm) """ obj = {} for c in colors: color_attr = "#%0.2X%0.2X%0.2X" % (c["r"], c["g"], c["b"]) obj[color_attr] = c return json.dumps(obj, indent=2) def render_svg(colors=None): """ renders all 64 colors, ignores provided colors (only there to share same signature with other functions """ polygons = [] dd = 300 for r in range(4): yy = r * dd xx = -742-dd for g in range(4): for b in range(4): xx += dd points = [(850,75), (958,137.5), (958,262.5), (850,325), (742,262.6), (742,137.5)] points = [(p[0]+xx, p[1]+yy) for p in points] points_attr = " ".join(["%f,%f" % (p[0], p[1]) for p in points]) color_attr = "#%0.2X%0.2X%0.2X" % (r * 85, g * 85, b * 85) polygon = """""" % (color_attr, points_attr) polygons.append(polygon) xml = """ %s """ % "\n".join(polygons) return xml if __name__ == "__main__": # e.g. --download_wikipedia --json snowy_colors.json --header ../src/fw/applib/graphics/gcolor_definitions.h parser = argparse.ArgumentParser(description="Generate various files that contain Snowy's 64 colors") parser.add_argument("--download_wikipedia", action='store_true', help="loads and caches colors from wikipedia") parser.add_argument("--download_colorlovers", action='store_true', help="loads and caches colors from colourlovers.com") parser.add_argument("--html", help="generates HTML file for test purposes") parser.add_argument("--json", help="generates JSON used by awesome Pebble color picker(tm)") parser.add_argument("--header", help="generates C header file that can replace color_definitions.h") parser.add_argument("--svg", help="generates SVG file with hexagons of all supported colors") if len(sys.argv) <= 1: parser.print_usage() sys.exit(1) args = parser.parse_args() if args.download_wikipedia: download_and_parse_colors_from_wikipedia() if args.download_colorlovers: download_all_colors_from_color_lovers() colors = all_colors_with_names() for k, v in {"html": render_html, "json": render_json, "header": render_header, "svg": render_svg}.items(): file_name = getattr(args, k) if file_name is not None: with open(file_name, "w") as f: f.write(v(colors))