Created
September 30, 2010 12:45
-
-
Save nathforge/604509 to your computer and use it in GitHub Desktop.
Manipulate RGB colours
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class RGBColor(object): | |
def __init__(self, value): | |
""" | |
A representation of an RGB color, allowing color manipulation. | |
Can take values in the form of strings, integers, | |
three-element iterables, or another RGBColor object. | |
>>> print RGBColor('#FFF') | |
#FFFFFF | |
>>> print RGBColor('#000000') | |
#000000 | |
>>> print RGBColor(0x123456) | |
#123456 | |
>>> print RGBColor((0xAA, 0xBB, 0xCC)) | |
#AABBCC | |
""" | |
if isinstance(value, basestring): | |
self.r, self.g, self.b = self._rgb_from_string(value) | |
elif isinstance(value, int): | |
self.r, self.g, self.b = self._rgb_from_int(value) | |
elif isinstance(value, RGBColor): | |
self.r, self.g, self.b = value.r, value.g, value.b | |
else: | |
self.r, self.g, self.b = value | |
@property | |
def rgb(self): | |
return (self.r, self.g, self.b) | |
def adjust(self, contrast_multiplier=1.0, brightness_offset=0.0, | |
hue_offset=0.0, saturation_multiplier=1.0): | |
yuv = self._yuv709_from_rgb(self.rgb) | |
adjusted_yuv = self._adjust_yuv(yuv, | |
contrast_multiplier, brightness_offset, | |
hue_offset, saturation_multiplier | |
) | |
rgb = self._rgb_from_yuv709(adjusted_yuv) | |
return RGBColor(rgb) | |
def opacity(self, value, background=0xFFFFFF): | |
""" | |
Fade a color towards a background color. Accepted values are strings | |
('0.9', '90%') or floats. Defaults to a white background (#FFFFFF). | |
>>> print RGBColor('#000000').opacity('90%') | |
#191919 | |
""" | |
if isinstance(value, basestring): | |
if value.endswith('%'): | |
value = float(value[:-1]) / 100 | |
else: | |
value = float(value) | |
if value < 0.0 or value > 1.0: | |
raise ValueError('value must be in the 0.0-1.0 range') | |
background = RGBColor(background) | |
return RGBColor( | |
background_primary + ((primary - background_primary) * value) | |
for primary, background_primary in zip(self.rgb, background.rgb) | |
) | |
def best_contrast_color(self, colors=(0x000000, 0xFFFFFF)): | |
""" | |
Find the color with the greatest contrast to ourselves. | |
Returns None if no colors are given. | |
Can be used to find the best text color for a given background. | |
Black and white: | |
>>> print RGBColor('#FFFFFF').best_contrast_color(('#000000', '#FFFFFF')) | |
#000000 | |
>>> print RGBColor('#000000').best_contrast_color(('#000000', '#FFFFFF')) | |
#FFFFFF | |
Full-intensity red, green and blue: | |
>>> print RGBColor('#FF0000').best_contrast_color(('#000000', '#FFFFFF')) | |
#000000 | |
>>> print RGBColor('#00FF00').best_contrast_color(('#000000', '#FFFFFF')) | |
#000000 | |
>>> print RGBColor('#0000FF').best_contrast_color(('#000000', '#FFFFFF')) | |
#FFFFFF | |
""" | |
best_contrast_ratio = 0 | |
best_color = None | |
for color in colors: | |
color = RGBColor(color) | |
contrast_ratio = self.contrast_ratio(color) | |
if contrast_ratio > best_contrast_ratio: | |
best_contrast_ratio = contrast_ratio | |
best_color = color | |
return best_color | |
def contrast_ratio(self, other): | |
""" | |
Calculate contrast ratio between ourselves and another color. | |
<http://www.w3.org/TR/WCAG20/#contrast-ratiodef> | |
""" | |
other = RGBColor(other) | |
l = self.relative_luminance() | |
other_l = other.relative_luminance() | |
light_l = max(l, other_l) | |
dark_l = min(l, other_l) | |
return (light_l + 0.05) / (dark_l + 0.05) | |
def relative_luminance(self): | |
""" | |
Calculate our relative luminance. | |
<http://www.w3.org/TR/WCAG20/#relativeluminancedef> | |
""" | |
srgb_r = self.r / 255.0 | |
srgb_g = self.g / 255.0 | |
srgb_b = self.b / 255.0 | |
r = srgb_r / 12.92 if srgb_r <= 0.03928 else ((srgb_r + 0.055) / 1.055) ** 2.4 | |
g = srgb_g / 12.92 if srgb_g <= 0.03928 else ((srgb_g + 0.055) / 1.055) ** 2.4 | |
b = srgb_b / 12.92 if srgb_b <= 0.03928 else ((srgb_b + 0.055) / 1.055) ** 2.4 | |
l = 0.2126 * r + 0.7152 * g + 0.0722 * b | |
return l | |
def gradient(self, other, steps): | |
""" | |
Calculate the colors needed to draw a gradient from this color to | |
another, in the given amount of steps. | |
>>> print ', '.join(map(str, RGBColor('#000000').gradient('#FFFFFF', steps=8))) | |
#000000, #242424, #484848, #6D6D6D, #919191, #B6B6B6, #DADADA, #FFFFFF | |
>>> print ', '.join(map(str, RGBColor('#0077FF').gradient('#FF7700', steps=8))) | |
#0077FF, #2477DA, #4877B6, #6D7791, #91776D, #B67748, #DA7724, #FF7700 | |
""" | |
other = RGBColor(other) | |
multipliers = tuple( | |
(end_primary - start_primary) / max(1.0, float(steps - 1)) | |
for start_primary, end_primary in zip(self.rgb, other.rgb) | |
) | |
return tuple( | |
RGBColor( | |
int(start_primary + (position * multiplier)) | |
for start_primary, multiplier | |
in zip(self.rgb, multipliers) | |
) | |
for position in xrange(steps) | |
) | |
@classmethod | |
def _adjust_yuv(cls, (y, u, v), contrast_multiplier, brightness_offset, | |
hue_offset, saturation_multiplier): | |
if hue_offset != 0.0: | |
hue_cos, hue_sin = math.cos(hue_offset), math.sin(hue_offset) | |
u = (u * hue_cos) + (v * hue_sin) | |
v = (v * hue_cos) - (u * hue_sin) | |
saturation_multiplier = max(0.0, saturation_multiplier) | |
y = max(0.0, min(1.0, (y * contrast_multiplier) + brightness_offset)) | |
u = u * contrast_multiplier * saturation_multiplier | |
v = v * contrast_multiplier * saturation_multiplier | |
return (y, u, v) | |
@classmethod | |
def _yuv709_from_rgb(cls, (r, g, b)): | |
""" | |
Convert an (r,g,b) tuple to a (y,u,v) tuple, using the BT.709 matrix. | |
""" | |
r, g, b = r / 255.0, g / 255.0, b / 255.0 | |
y = ( 0.2126 * r) + ( 0.7152 * g) + ( 0.0722 * b) | |
u = (-0.09991 * r) + (-0.33609 * g) + ( 0.436 * b) | |
v = ( 0.615 * r) + (-0.55861 * g) + (-0.05639 * b) | |
return (y, u, v) | |
@classmethod | |
def _rgb_from_yuv709(cls, (y, u, v)): | |
""" | |
Convert a (y,u,v) tuple to an (r,g,b) tuple, using the BT.709 matrix. | |
""" | |
r = max(0.0, min(1.0, y + ( 1.28033 * v))) | |
g = max(0.0, min(1.0, y + (-0.21482 * u) + (-0.38059 * v))) | |
b = max(0.0, min(1.0, y + ( 2.12798 * u) )) | |
r, g, b = int(r * 255.0), int(g * 255.0), int(b * 255.0) | |
return (r, g, b) | |
@classmethod | |
def _rgb_from_string(cls, string): | |
""" | |
Convert strings in the format "#123456", "#123", "123456" or "123" | |
into an (r,g,b) tuple. | |
""" | |
if string.startswith('#'): | |
string = string[1:] | |
if len(string) == 3: | |
r_hex, g_hex, b_hex = [hex_str + hex_str for hex_str in string] | |
elif len(string) == 6: | |
r_hex, g_hex, b_hex = string[:2], string[2:4], string[4:] | |
else: | |
raise ValueError('Hex string must be 3 or 6 characters, ' | |
'not including the leading hash') | |
return (int(r_hex, 16), int(g_hex, 16), int(b_hex, 16),) | |
@classmethod | |
def _rgb_from_int(cls, value): | |
""" | |
Convert a 24-bit RGB integer value into an (r,g,b) tuple. | |
""" | |
return ( | |
(value & 0xFF0000) >> 16, | |
(value & 0x00FF00) >> 8, | |
(value & 0x0000FF), | |
) | |
def __setattr__(self, name, value): | |
""" | |
Require valid values for r, g, b. | |
""" | |
if name in ('r', 'g', 'b'): | |
value = int(value) | |
if value < 0x00 or value > 0xFF: | |
raise ValueError('%s must be in the 0x00-0xFF range' % (name,)) | |
super(RGBColor, self).__setattr__(name, value) | |
def __repr__(self): | |
return '%s((0x%02X, 0x%02X, 0x%02X))' \ | |
% (self.__class__.__name__, self.r, self.g, self.b) | |
def __str__(self): | |
return '#%02X%02X%02X' % (self.r, self.g, self.b) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment