pebble/tools/snowy_colors.py
2025-01-27 11:38:16 -08:00

456 lines
16 KiB
Python
Executable file

#!/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(
"//! <span class=\"gcolor_sample\" style=\"background-color: %s;\"></span> <a href=\"https://developer.getpebble.com/tools/color-picker/%s\">%s</a>"
% (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 <TINTIN_ROOT>/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 = '<table style="border-spacing:0"><thead>'
html += '<tr><th colspan="4">Closest</th><th colspan="7">Actual Color</th></tr>'
html += "<tr>%s</tr>" % "".join(["<th>%s</th>" % s for s in ["r", "g", "b", "color", "color", "&Delta;", "r", "g", "b", "c code", "name", "identifier"]])
html += "</thead><tbody>"
for c in colors:
def rgb(c):
return '<td>%d</td><td>%d</td><td>%d</td>' % (c["r"], c["g"], c["b"])
def color(c):
return '<td style="background-color:rgb(%d,%d,%d); width:4em;"></td>' % (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 += '<tr>'
html += rgb(c["closest"])
html += color(c["closest"])
html += color(c)
html += '<td><strong>%d</strong></td>' % c["dist"]
html += rgb(c)
html += '<td><pre>'+c_code(c)+'</pre></td>'
html += '<td>'
if "url" in c:
html += '<a href="%s">%s</a>' % (c["url"], c["name"])
else:
html += c["name"]
html += '<td>%s</td>' % c["identifier"]
html += "</tr>"
html += "</tbody></table>"
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 = """<polygon fill="%s" stroke="black" stroke-width=".1" points="%s" />""" % (color_attr, points_attr)
polygons.append(polygon)
xml = """<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg viewBox="0 0 4000 4000"
xmlns="http://www.w3.org/2000/svg" version="1.1">
%s
</svg>""" % "\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))