From b7d436712010bc9cad96691cf99c86a3faf4f5fe Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Thu, 4 May 2017 12:22:49 +0800 Subject: [PATCH 01/15] new: pkg: setup branch coverage. --- setup.cfg | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.cfg b/setup.cfg index a6616d0..5b9f39a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,3 +57,7 @@ with-coverage = 1 cover-package = colour cover-min-percentage = 90 doctest-options = +ELLIPSIS,+NORMALIZE_WHITESPACE + + +[coverage:run] +branch = True \ No newline at end of file From 044aa6e3213e3c3f2c5f64b680a4293252968425 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Wed, 3 May 2017 10:57:43 +0800 Subject: [PATCH 02/15] chg: pkg: added doctest-only tests check. !minor This is to ensure minimal dependency towards nosetests and means that even if only the ``colour.py`` is copied in a project, it will be testable with ``python -m doctest colour.py``. --- .travis.yml | 7 ++++++- appveyor.yml | 7 ++++++- colour.py | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index fe9f9a5..eac8532 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,12 @@ install: - pip install coverage - python setup.py develop easy_install "$(./autogen.sh --get-name)[test]" ## getting deps script: - - nosetests -sx . + - nosetests -sx . ## will collect coverage reports + ## yes, the following is redundant and weaker than nosetests, but + ## for now I want to ensure that it works with a simpler + ## configuration: + - python -m doctest colour.py + - python -m doctest README.rst after_success: - "bash <(curl -s https://codecov.io/bash) #dovis: ignore" diff --git a/appveyor.yml b/appveyor.yml index a2a03cc..4b0c367 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -37,7 +37,12 @@ before_test: test_script: ## real tests - - nosetests -sx . + - nosetests -sx . ## collects coverage reports + ## yes, the following is redundant and weaker than nosetests, but + ## for now I want to ensure that it works with a simpler + ## configuration: + - python -m doctest colour.py + - python -m doctest README.rst after_test: - "codecov & REM #dovis: ignore" diff --git a/colour.py b/colour.py index fd21013..d2d0a2a 100644 --- a/colour.py +++ b/colour.py @@ -414,6 +414,7 @@ def rgb2hsl(rgb): red, green and blue: >>> rgb2hsl((0.9999999999999999, 1.0, 0.9999999999999994)) + ... ## doctest: +ELLIPSIS (0.0, 0.0, 0.999...) Of course: From 96bcac4e8994fb400dc478551bf53e42cd832b5a Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Wed, 3 May 2017 17:07:19 +0800 Subject: [PATCH 03/15] new: pkg: added dovis coverage collection stance. !minor --- .travis.yml | 2 +- appveyor.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index eac8532..dc558a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ script: - python -m doctest README.rst after_success: - "bash <(curl -s https://codecov.io/bash) #dovis: ignore" - + - if [ "$DOVIS" -a -d "$ARTIFACT_DIR" ]; then cp ".coverage" "$ARTIFACT_DIR"; echo "$PWD" > "$ARTIFACT_DIR/cover_path"; fi ## Ignored by Travis, but used internally to check packaging dist_check: diff --git a/appveyor.yml b/appveyor.yml index 4b0c367..e6b7c51 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -46,3 +46,8 @@ test_script: after_test: - "codecov & REM #dovis: ignore" + - | + IF DEFINED DOVIS IF DEFINED ARTIFACT_DIR ( + cp .coverage %ARTIFACT_DIR% + echo %cd% > %ARTIFACT_DIR%/cover_path + ) \ No newline at end of file From d8c2e261b11914e627e8165b8a952d6141b99824 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Wed, 12 Apr 2017 10:11:29 +0800 Subject: [PATCH 04/15] new: use namedtuple for rgb and hsl tuples. (fixes #22) --- README.rst | 42 +++++++++++++--- colour.py | 142 +++++++++++++++++++++++++++++++---------------------- 2 files changed, 118 insertions(+), 66 deletions(-) diff --git a/README.rst b/README.rst index 7e1d4c9..e0a3de3 100644 --- a/README.rst +++ b/README.rst @@ -37,6 +37,9 @@ Converts and manipulates common color representation (RGB, HSL, web, ...) Feature ======= +- one small file package, no dependencies, 100% tests full coverage, + fully documented. + - Damn simple and pythonic way to manipulate color representation (see examples below) @@ -54,10 +57,18 @@ Feature - can pick colors for you to identify objects of your application. - .. _W3C color naming: http://www.w3.org/TR/css3-color/#svg-color +Requirements +============ + +``colour`` is compatible Python 2 and Python 3 on +Linux/BSD/MacOSX and Windows. + +Please submit an issue if you encounter incompatibilities. + + Installation ============ @@ -122,12 +133,14 @@ Several representations are accessible:: >>> c.hex '#00f' >>> c.hsl # doctest: +ELLIPSIS - (0.66..., 1.0, 0.5) + Hsl(hue=0.66..., saturation=1.0, luminance=0.5) >>> c.rgb - (0.0, 0.0, 1.0) + Rgb(red=0.0, green=0.0, blue=1.0) -And their different parts are also independently accessible, as the different -amount of red, blue, green, in the RGB format:: +These two last are ``namedtuple`` and can be used as normal tuples. + +And their different sub values are also independently accessible, as +the different amount of red, blue, green, in the RGB format:: >>> c.red 0.0 @@ -343,6 +356,7 @@ And inequality (using ``__ne__``) are also polite:: Picking arbitrary color for a python object ------------------------------------------- + Basic Usage ~~~~~~~~~~~ @@ -363,6 +377,7 @@ same for same objects, and different for different object:: Of course, although there's a tiny probability that different strings yield the same color, most of the time, different inputs will produce different colors. + Advanced Usage ~~~~~~~~~~~~~~ @@ -390,9 +405,22 @@ Thus:: >>> my_obj_color == my_str_color False +And with unhashable types... here we consider as equivalent two +instances with same ``str`` representation:: + + >>> class MyObj(dict): pass + >>> my_dict = MyObj(foo=1) + >>> my_obj_color = Color(pick_for=my_dict) + >>> new_dict = MyObj() ## new_dict has not the same content as my_dict yet + >>> Color(pick_for=my_dict) == Color(pick_for=new_dict) + False + >>> new_dict["foo"] = 1 ## now they have equivalent string representation + >>> Color(pick_for=my_dict) == Color(pick_for=new_dict) + True + Please make sure your object is hashable or "stringable" before using the ``RGB_color_picker`` picking mechanism or provide another color picker. Nearly -all python object are hashable by default so this shouldn't be an issue (e.g. +all python object are hashable by default so this shouldn't be an issue (e.g. instances of ``object`` and subclasses are hashable). Neither ``hash`` nor ``str`` are perfect solution. So feel free to use @@ -436,7 +464,7 @@ would get the specified attributes by default:: >>> black_red = get_color("red", luminance=0) >>> black_blue = get_color("blue", luminance=0) -Of course, these are always instances of ``Color`` class:: +Of course, these are still instances of ``Color`` class:: >>> isinstance(black_red, Color) True diff --git a/colour.py b/colour.py index d2d0a2a..8727cfe 100644 --- a/colour.py +++ b/colour.py @@ -36,6 +36,7 @@ from __future__ import with_statement, print_function +import collections import hashlib import re import sys @@ -224,9 +225,9 @@ class C_RGB: >>> from colour import RGB >>> RGB.WHITE - (1.0, 1.0, 1.0) + Rgb(red=1.0, green=1.0, blue=1.0) >>> RGB.BLUE - (0.0, 0.0, 1.0) + Rgb(red=0.0, green=0.0, blue=1.0) >>> RGB.DONOTEXISTS # doctest: +ELLIPSIS Traceback (most recent call last): @@ -265,6 +266,14 @@ def __getattr__(self, value): HEX = C_HEX() +## +## Types +## + +Hsl = collections.namedtuple("Hsl", ["hue", "saturation", "luminance"]) +Rgb = collections.namedtuple("Rgb", ["red", "green", "blue"]) + + ## ## Convertion function ## @@ -294,41 +303,41 @@ def hsl2rgb(hsl): With a lightness put at 0, RGB is always rgbblack >>> hsl2rgb((0.0, 0.0, 0.0)) - (0.0, 0.0, 0.0) + Rgb(red=0.0, green=0.0, blue=0.0) >>> hsl2rgb((0.5, 0.0, 0.0)) - (0.0, 0.0, 0.0) + Rgb(red=0.0, green=0.0, blue=0.0) >>> hsl2rgb((0.5, 0.5, 0.0)) - (0.0, 0.0, 0.0) + Rgb(red=0.0, green=0.0, blue=0.0) Same for lightness put at 1, RGB is always rgbwhite >>> hsl2rgb((0.0, 0.0, 1.0)) - (1.0, 1.0, 1.0) + Rgb(red=1.0, green=1.0, blue=1.0) >>> hsl2rgb((0.5, 0.0, 1.0)) - (1.0, 1.0, 1.0) + Rgb(red=1.0, green=1.0, blue=1.0) >>> hsl2rgb((0.5, 0.5, 1.0)) - (1.0, 1.0, 1.0) + Rgb(red=1.0, green=1.0, blue=1.0) With saturation put at 0, the RGB should be equal to Lightness: >>> hsl2rgb((0.0, 0.0, 0.25)) - (0.25, 0.25, 0.25) + Rgb(red=0.25, green=0.25, blue=0.25) >>> hsl2rgb((0.5, 0.0, 0.5)) - (0.5, 0.5, 0.5) + Rgb(red=0.5, green=0.5, blue=0.5) >>> hsl2rgb((0.5, 0.0, 0.75)) - (0.75, 0.75, 0.75) + Rgb(red=0.75, green=0.75, blue=0.75) With saturation put at 1, and lightness put to 0.5, we can find normal full red, green, blue colors: >>> hsl2rgb((0 , 1.0, 0.5)) - (1.0, 0.0, 0.0) + Rgb(red=1.0, green=0.0, blue=0.0) >>> hsl2rgb((1 , 1.0, 0.5)) - (1.0, 0.0, 0.0) + Rgb(red=1.0, green=0.0, blue=0.0) >>> hsl2rgb((1.0/3 , 1.0, 0.5)) - (0.0, 1.0, 0.0) + Rgb(red=0.0, green=1.0, blue=0.0) >>> hsl2rgb((2.0/3 , 1.0, 0.5)) - (0.0, 0.0, 1.0) + Rgb(red=0.0, green=0.0, blue=1.0) Of course: >>> hsl2rgb((0.0, 2.0, 0.5)) # doctest: +ELLIPSIS @@ -351,7 +360,7 @@ def hsl2rgb(hsl): raise ValueError("Lightness must be between 0 and 1.") if s == 0: - return l, l, l + return Rgb(l, l, l) if l < 0.5: v2 = l * (1.0 + s) @@ -364,7 +373,7 @@ def hsl2rgb(hsl): g = _hue2rgb(v1, v2, h) b = _hue2rgb(v1, v2, h - (1.0 / 3)) - return r, g, b + return Rgb(r, g, b) def rgb2hsl(rgb): @@ -387,35 +396,35 @@ def rgb2hsl(rgb): >>> rgb2hsl((1.0, 1.0, 1.0)) # doctest: +ELLIPSIS - (..., 0.0, 1.0) + Hsl(hue=..., saturation=0.0, luminance=1.0) >>> rgb2hsl((0.5, 0.5, 0.5)) # doctest: +ELLIPSIS - (..., 0.0, 0.5) + Hsl(hue=..., saturation=0.0, luminance=0.5) >>> rgb2hsl((0.0, 0.0, 0.0)) # doctest: +ELLIPSIS - (..., 0.0, 0.0) + Hsl(hue=..., saturation=0.0, luminance=0.0) If only one color is different from the others, it defines the direct Hue: >>> rgb2hsl((0.5, 0.5, 1.0)) # doctest: +ELLIPSIS - (0.66..., 1.0, 0.75) + Hsl(hue=0.66..., saturation=1.0, luminance=0.75) >>> rgb2hsl((0.2, 0.1, 0.1)) # doctest: +ELLIPSIS - (0.0, 0.33..., 0.15...) + Hsl(hue=0.0, saturation=0.33..., luminance=0.15...) Having only one value set, you can check that: >>> rgb2hsl((1.0, 0.0, 0.0)) - (0.0, 1.0, 0.5) + Hsl(hue=0.0, saturation=1.0, luminance=0.5) >>> rgb2hsl((0.0, 1.0, 0.0)) # doctest: +ELLIPSIS - (0.33..., 1.0, 0.5) + Hsl(hue=0.33..., saturation=1.0, luminance=0.5) >>> rgb2hsl((0.0, 0.0, 1.0)) # doctest: +ELLIPSIS - (0.66..., 1.0, 0.5) + Hsl(hue=0.66..., saturation=1.0, luminance=0.5) Regression check upon very close values in every component of red, green and blue: >>> rgb2hsl((0.9999999999999999, 1.0, 0.9999999999999994)) ... ## doctest: +ELLIPSIS - (0.0, 0.0, 0.999...) + Hsl(hue=0.0, saturation=0.0, luminance=0.999...) Of course: @@ -447,7 +456,7 @@ def rgb2hsl(rgb): l = vsum / 2 if diff < FLOAT_ERROR: ## This is a gray, no chroma... - return (0.0, 0.0, l) + return Hsl(0.0, 0.0, l) ## ## Chromatic data... @@ -473,7 +482,7 @@ def rgb2hsl(rgb): if h < 0: h += 1 if h > 1: h -= 1 - return (h, s, l) + return Hsl(h, s, l) def _hue2rgb(v1, v2, vH): @@ -493,6 +502,32 @@ def _hue2rgb(v1, v2, vH): return v1 +def hexl2hexs(hex): + """Shorten from 6 to 3 hex represention if possible + + Usage + ----- + + >>> from colour import rgb2hex + + Provided a long string hex format, it should shorten it when + possible:: + + >>> hexl2hexs('#00ff00') + '#0f0' + + In the following case, it is not possible to shorten, thus:: + + >>> hexl2hexs('#01ff00') + '#01ff00' + + """ + + if len(hex) == 7 and hex[1::2] == hex[2::2]: + return "#" + ''.join(hex[1::2]) + return hex + + def rgb2hex(rgb, force_long=False): """Transform RGB tuple to hex RGB representation @@ -522,13 +557,9 @@ def rgb2hex(rgb, force_long=False): """ - hx = ''.join(["%02x" % int(c * 255 + 0.5 - FLOAT_ERROR) - for c in rgb]) - - if not force_long and hx[0::2] == hx[1::2]: - hx = ''.join(hx[0::2]) - - return "#%s" % hx + hx = "#" + ''.join(["%02x" % int(c * 255 + 0.5 - FLOAT_ERROR) + for c in rgb]) + return hx if force_long else hexl2hexs(hx) def hex2rgb(str_rgb): @@ -540,18 +571,18 @@ def hex2rgb(str_rgb): >>> from colour import hex2rgb >>> hex2rgb('#00ff00') - (0.0, 1.0, 0.0) + Rgb(red=0.0, green=1.0, blue=0.0) >>> hex2rgb('#0f0') - (0.0, 1.0, 0.0) + Rgb(red=0.0, green=1.0, blue=0.0) >>> hex2rgb('#aaa') # doctest: +ELLIPSIS - (0.66..., 0.66..., 0.66...) + Rgb(red=0.66..., green=0.66..., blue=0.66...) >>> hex2rgb('#aa') # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: Invalid value '#aa' provided for rgb color. + ValueError: Invalid value '#aa' provided as hex color for rgb conversion. """ @@ -565,10 +596,11 @@ def hex2rgb(str_rgb): else: raise ValueError() except: - raise ValueError("Invalid value %r provided for rgb color." - % str_rgb) + raise ValueError( + "Invalid value %r provided as hex color for rgb conversion." + % str_rgb) - return tuple([float(int(v, 16)) / 255 for v in (r, g, b)]) + return Rgb(*[float(int(v, 16)) / 255 for v in (r, g, b)]) def hex2web(hex): @@ -606,13 +638,7 @@ def hex2web(hex): return color_name if len(re.sub(r"[^A-Z]", "", color_name)) > 1 \ else color_name.lower() - # Hex format is verified by hex2rgb function. And should be 3 or 6 digit - if len(hex) == 7: - if hex[1] == hex[2] and \ - hex[3] == hex[4] and \ - hex[5] == hex[6]: - return '#' + hex[1] + hex[3] + hex[5] - return hex + return hexl2hexs(hex) def web2hex(web, force_long=False): @@ -675,8 +701,6 @@ def web2hex(web, force_long=False): if web not in COLOR_NAME_TO_RGB: raise ValueError("%r is not a recognized color." % web) - ## convert dec to hex: - return rgb2hex([float(int(v)) / 255 for v in COLOR_NAME_TO_RGB[web]], force_long) @@ -788,7 +812,7 @@ def hash_or_str(obj): except TypeError: ## Adds the type name to make sure two object of different type but ## identical string representation get distinguished. - return type(obj).__name__ + str(obj) + return "\0".join([type(obj).__name__, str(obj)]) ## @@ -824,9 +848,9 @@ class Color(object): 0.0 >>> b.rgb - (0.0, 0.0, 1.0) + Rgb(red=0.0, green=0.0, blue=1.0) >>> b.hsl # doctest: +ELLIPSIS - (0.66..., 1.0, 0.5) + Hsl(hue=0.66..., saturation=1.0, luminance=0.5) >>> b.hex '#00f' @@ -847,7 +871,7 @@ class Color(object): >>> b.hex = '#f00' >>> b.hsl - (0.0, 1.0, 0.5) + Hsl(hue=0.0, saturation=1.0, luminance=0.5) Long hex can be accessed directly: @@ -875,9 +899,9 @@ class Color(object): >>> c.saturation = 0.0 >>> c.hsl # doctest: +ELLIPSIS - (..., 0.0, 0.5) + Hsl(hue=..., saturation=0.0, luminance=0.5) >>> c.rgb - (0.5, 0.5, 0.5) + Rgb(red=0.5, green=0.5, blue=0.5) >>> c.hex '#7f7f7f' >>> c @@ -967,7 +991,7 @@ class Color(object): And keep the internal API working:: >>> Tint("red").hsl - (0.0, 1.0, 0.5) + Hsl(hue=0.0, saturation=1.0, luminance=0.5) """ @@ -1013,7 +1037,7 @@ def __setattr__(self, label, value): ## def get_hsl(self): - return tuple(self._hsl) + return Hsl(*self._hsl) def get_hex(self): return rgb2hex(self.rgb) From 13295f4fdaaf3798e7e6e10acf6d13cdfd6f7a2a Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Wed, 12 Apr 2017 11:51:26 +0800 Subject: [PATCH 05/15] new: dev: refactoring ``get/set`` towards independance with formats. The aim here is to be able to plug formats at will. The Color object will then give easy access to given formats. Thus, it should have no previous knowledge of names of formats nor inner attributes. The remaining attributes will be removed in upcoming commits. --- colour.py | 129 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 67 insertions(+), 62 deletions(-) diff --git a/colour.py b/colour.py index 8727cfe..aa09dd5 100644 --- a/colour.py +++ b/colour.py @@ -274,6 +274,34 @@ def __getattr__(self, value): Rgb = collections.namedtuple("Rgb", ["red", "green", "blue"]) +def format_get(label, formats): + for f in formats: + if label == format_id(f): + return f + return None + + +def format_id(f): + if isinstance(f, type): + return f.__name__.lower() + else: + return f + + +def format_get_by_attr(label, formats): + for f in formats: + for attr in getattr(f, "_fields", []): + if label == attr: + return f + return None + + +def format_find(label, formats): + f = format_get(label, formats) + if f is not None: + return f, None + return format_get_by_attr(label, formats), label + ## ## Convertion function ## @@ -996,6 +1024,7 @@ class Color(object): """ _hsl = None ## internal representation + _FORMATS = [Rgb, Hsl, "hex", "web"] def __init__(self, color=None, pick_for=None, picker=RGB_color_picker, pick_key=hash_or_str, @@ -1019,6 +1048,20 @@ def __init__(self, color=None, def __getattr__(self, label): if label.startswith("get_"): + l = label[4:] + f, attr = format_find(l, self._FORMATS) + if f is not None: + current_format = Hsl + if attr is not None: + return lambda: getattr( + getattr(self, format_id(f)), + attr) + function_label = "%s2%s" % (format_id(current_format), + format_id(f)) + fun = globals().get(function_label) + if fun: + return lambda: fun( + getattr(self, format_id(current_format))) raise AttributeError("'%s' not found" % label) try: return getattr(self, 'get_' + label)() @@ -1026,11 +1069,30 @@ def __getattr__(self, label): raise AttributeError("'%s' not found" % label) def __setattr__(self, label, value): - if label not in ["_hsl", "equality"]: - fc = getattr(self, 'set_' + label) - fc(value) - else: + if label in ["_hsl", "equality"]: self.__dict__[label] = value + return + + method_label = 'set_' + label + if hasattr(self, method_label): + fc = getattr(self, method_label) + fc(value) + return + f, attr = format_find(label, self._FORMATS) + if f is not None: + current_format = Hsl + if attr is not None: + setattr(self, format_id(f), + getattr(self, format_id(f))._replace(**{attr: value})) + return + function_label = "%s2%s" % (format_id(f), + format_id(current_format)) + fun = globals().get(function_label) + if fun: + setattr(self, format_id(current_format), fun(value)) + return + + raise AttributeError(label) ## ## Get @@ -1039,36 +1101,9 @@ def __setattr__(self, label, value): def get_hsl(self): return Hsl(*self._hsl) - def get_hex(self): - return rgb2hex(self.rgb) - def get_hex_l(self): return rgb2hex(self.rgb, force_long=True) - def get_rgb(self): - return hsl2rgb(self.hsl) - - def get_hue(self): - return self.hsl[0] - - def get_saturation(self): - return self.hsl[1] - - def get_luminance(self): - return self.hsl[2] - - def get_red(self): - return self.rgb[0] - - def get_green(self): - return self.rgb[1] - - def get_blue(self): - return self.rgb[2] - - def get_web(self): - return hex2web(self.hex) - ## ## Set ## @@ -1076,37 +1111,7 @@ def get_web(self): def set_hsl(self, value): self._hsl = list(value) - def set_rgb(self, value): - self.hsl = rgb2hsl(value) - - def set_hue(self, value): - self._hsl[0] = value - - def set_saturation(self, value): - self._hsl[1] = value - - def set_luminance(self, value): - self._hsl[2] = value - - def set_red(self, value): - _, g, b = self.rgb - self.rgb = (value, g, b) - - def set_green(self, value): - r, _, b = self.rgb - self.rgb = (r, value, b) - - def set_blue(self, value): - r, g, _ = self.rgb - self.rgb = (r, g, value) - - def set_hex(self, value): - self.rgb = hex2rgb(value) - - set_hex_l = set_hex - - def set_web(self, value): - self.hex = web2hex(value) + set_hex_l = lambda s, v: setattr(s, "hex", v) ## range of color generation From 82be2302060e71016af182d1b81af7814af3109f Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Tue, 18 Apr 2017 16:13:05 +0800 Subject: [PATCH 06/15] new: engine rewrite to provide easier writing of new formats. API changes: - attribute ``hex_l`` removed in favor of ``hex`` which is now long (6 hexadigit long). Use ``hexs`` for the possibly shortened version to 3 hex digit. - ``LONG_HEX_COLOR`` and ``SHORT_HEX_COLOR`` are not available in module's scope anymore, but are moved in each corresponding format's attribute (``Hex.regex``, and ``HexS.regex``). - ``HEX`` helper class is renamed ``Hex``. - ``xxx2yyy`` conversion functions are unaware of any format and work with basic python types and returns basic python types. --- README.rst | 18 +- colour.py | 1506 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 1101 insertions(+), 423 deletions(-) diff --git a/README.rst b/README.rst index e0a3de3..e5f0768 100644 --- a/README.rst +++ b/README.rst @@ -131,11 +131,15 @@ Reading values Several representations are accessible:: >>> c.hex + '#0000ff' + >>> c.hexs '#00f' + >>> c.web + 'blue' >>> c.hsl # doctest: +ELLIPSIS - Hsl(hue=0.66..., saturation=1.0, luminance=0.5) + HSL(hue=0.66..., saturation=1.0, luminance=0.5) >>> c.rgb - Rgb(red=0.0, green=0.0, blue=1.0) + RGB(red=0.0, green=0.0, blue=1.0) These two last are ``namedtuple`` and can be used as normal tuples. @@ -158,12 +162,12 @@ Or the hue, saturation and luminance of the HSL representation:: >>> c.luminance 0.5 -A note on the ``.hex`` property, it'll return the smallest valid value -when possible. If you are only interested by the long value, use -``.hex_l``:: +A note on the ``.hex`` property: it'll return the 6 hexadigit, if you +needed the version of this format that allow short 3 hexadigit when possible, +use ``hexs`` format:: - >>> c.hex_l - '#0000ff' + >>> c.hexs + '#00f' Modifying color objects diff --git a/colour.py b/colour.py index aa09dd5..4ceffed 100644 --- a/colour.py +++ b/colour.py @@ -6,32 +6,16 @@ This module defines several color formats that can be converted to one or another. -Formats -------- - -HSL: - 3-uple of Hue, Saturation, Lightness all between 0.0 and 1.0 - -RGB: - 3-uple of Red, Green, Blue all between 0.0 and 1.0 - -HEX: - string object beginning with '#' and with red, green, blue value. - This format accept color in 3 or 6 value ex: '#fff' or '#ffffff' - -WEB: - string object that defaults to HEX representation or human if possible - Usage ----- Several function exists to convert from one format to another. But all -function are not written. So the best way is to use the object Color. +convertion function are not written from all format towards all formats. +By using Color object, you'll silently use all available function, sometime +chained, to give you a convertion from any format to any other. Please see the documentation of this object for more information. -.. note:: Some constants are defined for convenience in HSL, RGB, HEX - """ from __future__ import with_statement, print_function @@ -40,7 +24,22 @@ import hashlib import re import sys +import traceback +import inspect + + +## +## Convenience function +## +def with_metaclass(mcls): + def decorator(cls): + body = vars(cls).copy() + # clean out class body + body.pop('__dict__', None) + body.pop('__weakref__', None) + return mcls(cls.__name__, cls.__bases__, body) + return decorator ## ## Some Constants @@ -200,112 +199,995 @@ for name in names) -LONG_HEX_COLOR = re.compile(r'^#[0-9a-fA-F]{6}$') -SHORT_HEX_COLOR = re.compile(r'^#[0-9a-fA-F]{3}$') +## +## Color Type Factory machinery +## + + +class FormatRegistry(list): + + def get(self, label, default=None): + for f in self: + if label == str(f): + return f + return default + + def find(self, label): + f = self.get(label, None) + if f is not None: + return f, None + return self.get_by_attr(label), label + + def get_by_attr(self, label): + for f in self: + for attr in getattr(f, "_fields", []): + if label == attr: + return f + return None + + +def register_format(registry): + def wrap(f): + registry.append(f) + return f + return wrap + + +class MetaFormat(type): + + def __getattr__(self, value): + label = value.lower() + if label in COLOR_NAME_TO_RGB: + return RGB( + tuple(v / 255. for v in COLOR_NAME_TO_RGB[label]) + ).convert(self) + raise AttributeError("%s instance has no attribute %r" + % (self.__class__, value)) + + def __repr__(self): + return "" % (self.__name__, ) + + def __str__(self): + return self.__name__.lower() + + +@with_metaclass(MetaFormat) +class Format(object): + + def convert(self, dst_format, converter_registry=None): + converter_registry = converter_registry or Converters + src_format = type(self) + ret = converter_registry.convert_fun( + src_format, dst_format)(self) + return ret + + +def Tuple(*a): + """Create a simpler named tuple type, inheriting from ``Format`` + + Usage + ----- + + >>> Tuple("a", "b", "c") + + + This can conveniently be used as a parent class for formats, with + an easy declaration:: + + >>> class MyFormat(Tuple("a", "b", "c")): pass + + .. sensible representation:: + + >>> MyFormat(1, 2, 3) + MyFormat(a=1, b=2, c=3) + + .. and the ability to take a real tuple upon initialisation:: + + >>> MyFormat((1, 2, 3)) + MyFormat(a=1, b=2, c=3) + + .. keeping the awesome feature of namedtuples, a partial argument + and keyword list:: + + >>> MyFormat(1, b=2, c=3) + MyFormat(a=1, b=2, c=3) + + Of course, these are subclasses of ``Format``:: + + >>> isinstance(MyFormat((1, 2, 3)), Format) + True + + """ + + class klass(collections.namedtuple("_Anon", a), Format): + ## Allow the support of instanciating with a single tuple. + def __new__(cls, *a, **kw): + if len(a) == 1 and isinstance(a[0], tuple) and \ + len(a[0]) == len(cls._fields): + return cls.__new__(cls, *a[0], **kw) + return super(klass, cls).__new__(cls, *a, **kw) + + ## Force namedtuple to read the actual name of the class + def __repr__(self): + return ('%s(%s)' + % (self.__class__.__name__, + ", ".join("%s=%r" % (f, getattr(self, f)) + for f in self._fields))) + + ## Provide a sensible name + klass.__name__ = "Tuple(%s)" % (", ".join(a), ) + + return klass + + +class String(str, Format): + """Defines a Format based on python string + + A default validation will be done on the class attribute + regex:: + + >>> class MyFormat(String): + ... regex = re.compile('(red|blue|green)') + + >>> MyFormat('invalid') + Traceback (most recent call last): + ... + ValueError: Invalid string specifier 'invalid' format for MyFormat format. + + >>> red = MyFormat('red') + + Notice that the representation of this object is invisible as it + is a subclass of string:: + + >>> red + 'red' + + Although:: + + >>> type(MyFormat('red')) + + + You can avoid setting ``regex`` if you have no use of this check:: + + >>> class MyFormat(String): pass + >>> MyFormat('red') + 'red' + + """ + + default = None ## no value + regex = None + + def __new__(cls, s, **kwargs): + if s is None and cls.default is not None: + s = cls.default + s = cls._validate(s) + return super(String, cls).__new__(cls, s) + + @classmethod + def _validate(cls, s): + if cls.regex: + if not cls.regex.match(s): + raise ValueError( + 'Invalid string specifier %r format for %s format.' + % (s, cls.__name__)) + return s + + +## +## Converters function +## + +class ConverterRegistry(list): + """Provides helper functions to get and combine converters function + + First, this object acts as a registry, storing in a list the available + converters. Converters are special annotated functions:: + + >>> cr = ConverterRegistry() + + Registering should be done thanks to ``register_converter`` decorator:: + + >>> @register_converter(cr, src="hex", dst="dec") + ... def hex2dec(x): return int(x, 16) + + Or equivalently:: + + >>> register_converter(cr, src="dec", dst="hex")( + ... lambda x: hex(x)) ## doctest: +ELLIPSIS + at ...> + >>> register_converter(cr, src="dec", dst="bin")( + ... lambda x: bin(x)) ## doctest: +ELLIPSIS + at ...> + + Then we can expect simply converting between available path:: + + >>> cr.convert_fun("hex", "dec")("15") + 21 + + Note that this is provided directly by only one converter, in the following + 2 converters will be used to get to the answer:: + + >>> cr.convert_fun("hex", "bin")("15") + '0b10101' + + When source and destination format are equivalent, this will make not change + on the output:: + + >>> cr.convert_fun("hex", "hex")("15") + '15' + + And if no path exists it'll cast an exception:: + + >>> cr.convert_fun("bin", "hex")("0101") + Traceback (most recent call last): + ... + ValueError: No convertion path found from bin to hex format. + + Note that if the functions have already been annotated, then you + can instantiate directly a new ``ConverterRegistry``:: + + >>> new_cr = ConverterRegistry(cr) + >>> new_cr.convert_fun("hex", "bin")("15") + '0b10101' + + """ + + def __init__(self, converters=None): + if converters is None: + converters = [] + super(ConverterRegistry, self).__init__(converters) + + def get(self, src): + return {cv.dst: (cv, cv.conv_kwargs) + for cv in self + if cv.src is src} + + def find_path(self, src, dst): + visited = [src] + nexts = [(([], n), t[0], t[1]) + for n, t in self.get(src).items() + if n not in visited] + while len(nexts) != 0: + (path, next), fun, dct = nexts.pop() + visited.append(next) + new_path = path + [fun] + dsts = self.get(next) + if dst is next: + return new_path + nexts.extend([((new_path, n), t[0], t[1]) + for n, t in dsts.items() if n not in visited]) + + def convert_fun(self, src_format, dst_format): + def _path_to_callable(path): + def _f(value): + for fun in path: + value = fun(value) + if callable(fun.dst): + value = fun.dst(value) + return value + return _f + if src_format is dst_format or src_format == dst_format: + return lambda x: x + path = self.find_path(src_format, dst_format) + if path: + return _path_to_callable(path) + raise ValueError( + "No convertion path found from %s to %s format." + % (src_format, dst_format)) + + +def register_converter(registry, src, dst, **kwargs): + + def decorator(f): + f.src = src + f.dst = dst + f.conv_kwargs = kwargs + registry.append(f) + return f + return decorator + + +def color_scale(begin_hsl, end_hsl, nb): + """Returns a list of nb color HSL tuples between begin_hsl and end_hsl + + >>> from colour import color_scale + + >>> [HSL(hsl).convert(HexS) for hsl in color_scale((0, 1, 0.5), + ... (1, 1, 0.5), 3)] + ['#f00', '#0f0', '#00f', '#f00'] + + >>> [HSL(hsl).convert(HexS) + ... for hsl in color_scale((0, 0, 0), + ... (0, 0, 1), + ... 15)] # doctest: +ELLIPSIS + ['#000', '#111', '#222', ..., '#ccc', '#ddd', '#eee', '#fff'] + + Of course, asking for negative values is not supported: + + >>> color_scale((0, 1, 0.5), (1, 1, 0.5), -2) + Traceback (most recent call last): + ... + ValueError: Unsupported negative number of colors (nb=-2). + + """ + + if nb < 0: + raise ValueError( + "Unsupported negative number of colors (nb=%r)." % nb) + + step = tuple([float(end_hsl[i] - begin_hsl[i]) / nb for i in range(0, 3)]) \ + if nb > 0 else (0, 0, 0) + + def mul(step, value): + return tuple([v * value for v in step]) + + def add_v(step, step2): + return tuple([v + step2[i] for i, v in enumerate(step)]) + + return [add_v(begin_hsl, mul(step, r)) for r in range(0, nb + 1)] + + +## +## Color Pickers +## + +def RGB_color_picker(obj): + """Build a color representation from the string representation of an object + + This allows to quickly get a color from some data, with the + additional benefit that the color will be the same as long as the + (string representation of the) data is the same:: + + >>> from colour import RGB_color_picker, Color + + Same inputs produce the same result:: + + >>> RGB_color_picker("Something") == RGB_color_picker("Something") + True + + ... but different inputs produce different colors:: + + >>> RGB_color_picker("Something") != RGB_color_picker("Something else") + True + + In any case, we still get a ``Color`` object:: + + >>> isinstance(RGB_color_picker("Something"), Color) + True + + """ + + ## Turn the input into a by 3-dividable string. SHA-384 is good because it + ## divides into 3 components of the same size, which will be used to + ## represent the RGB values of the color. + digest = hashlib.sha384(str(obj).encode('utf-8')).hexdigest() + + ## Split the digest into 3 sub-strings of equivalent size. + subsize = int(len(digest) / 3) + splitted_digest = [digest[i * subsize: (i + 1) * subsize] + for i in range(3)] + + ## Convert those hexadecimal sub-strings into integer and scale them down + ## to the 0..1 range. + max_value = float(int("f" * subsize, 16)) + components = ( + int(d, 16) ## Make a number from a list with hex digits + / max_value ## Scale it down to [0.0, 1.0] + for d in splitted_digest) + + return Color(rgb2hex(components)) ## Profit! + + +def hash_or_str(obj): + try: + return hash((type(obj).__name__, obj)) + except TypeError: + ## Adds the type name to make sure two object of different type but + ## identical string representation get distinguished. + return "\0".join([type(obj).__name__, str(obj)]) + +## +## All purpose object +## + +def mkDataSpace(formats, converters, picker=None, + internal_format=None, + input_formats=[], + repr_format=None, + string_format=None): + """Returns a DataSpace provided a format registry and converters + + To create a data space you'll need a format registry, as this one:: + + >>> fr = FormatRegistry() + + >>> @register_format(fr) + ... class Dec(int, Format): pass + >>> @register_format(fr) + ... class Hex(String): pass + >>> @register_format(fr) + ... class Bin(String): pass + + To create a data space you'll need a converter registry, as this one:: + + >>> cr = ConverterRegistry() + + >>> @register_converter(cr, Hex, Dec) + ... def h2d(x): return int(x, 16) + >>> @register_converter(cr, Dec, Hex) + ... def d2h(x): return hex(x) + >>> @register_converter(cr, Dec, Bin) + ... def d2b(x): return bin(x) + + Then you can create the data space:: + + >>> class Numeric(mkDataSpace(fr, cr)): pass + + + Instantiation + ============= + + You can instatiate by explicitely giving the input format: + + >>> Numeric(dec=1) + + >>> Numeric(hex='0xc') + + + Similarily, you can let the ``DataSpace`` object figure + it out if you provide an already instantiated value: + + >>> Numeric(Dec(1)) + + >>> Numeric(Hex('0xc')) + + + And you can instantiate a DataSpace object with an other instance of + it self:: + + >>> Numeric(Numeric(1)) + + + You can also let the dataspace try to figure it out thanks to + ``input_formats`` which is covered in the next section. + + And, finally, using the ``pick_for`` attribute, you can ask an + automatic value for any type of python object. This value would + then identify and should be always the same for the same object. + This is covered in ``picker`` section. + + + input_formats + ------------- + + A dataspace can be instantiated with any type of object, in the + case the object is not an instance of a format listed in the + format registry, it'll have to try to try to instantiate one of + these internal format with the value you have provided. This is + where the ``input_formats`` list will be used. Note that if it was + not specified it will be the ``repr_format`` alone and if not + specified, it'll fallback on the first available format alone in + the format registry: + + >>> class Numeric(mkDataSpace(fr, cr)): pass + >>> Numeric(1) + + + But notice that hex value will be refused, as the only input format + specified was (by default) the first one in the registry ``Dec``:: + + >>> Numeric('0xc') + Traceback (most recent call last): + ... + ValueError: No input formats are able to read '0xc' (tried Dec) + + So if you want, you can specify the list of input formats, they will be + tried in the given order... + + >>> class Numeric(mkDataSpace(fr, cr, input_formats=[Dec, Hex])): pass + >>> Numeric(1) + + >>> Numeric('0xc') + + + Note also that if you don't want to specify ``input_formats``, using keyword + can be allowed for one-time access:: + + >>> class Numeric(mkDataSpace(fr, cr)): pass + >>> Numeric(1) + + >>> Numeric(hex='0xc') + + + + picker + ------ + + By default, the picker mecanism is not operational:: + + >>> class Numeric(mkDataSpace(fr, cr)): pass + >>> Numeric(pick_for=object()) + Traceback (most recent call last): + ... + ValueError: Can't pick value as no picker was defined. + + You must define a ``picker``, a function that will output a value + that could be instantiated by the ``DataSpace`` object:: + + >>> class Numeric(mkDataSpace(fr, cr, picker=lambda x: 1)): pass + >>> Numeric(pick_for=object()) + + + Of course, this is a dummy example, you should probably use + ``hash`` or ``id`` or the string representation of your object to + reliably give a different value to different object while having + the same value for the same object. + + + Output formats + ============== + + Dataspace will have mainly 2 output formats to follow python conventions:: + - a repr output + - a string output + These are manageable independantly if needed. + + + object representation + --------------------- + + There's a ``repr_format`` keyword to set the python repr format, by + default it will fallback to the first format available in the format + registry:: + + >>> class Numeric(mkDataSpace(fr, cr, repr_format=Hex)): pass + >>> Numeric('0xc') + + + Notice that the input format by default is the ``repr_format``. So: + + >>> class Numeric(mkDataSpace(fr, cr, + ... repr_format=Hex, input_formats=[Dec, ])): pass + >>> Numeric(12) + + + + object string output + -------------------- + + There's a ``string_format`` keyword to set the python string + output used by ``%s`` or ``str()``, by default it will fallback to + the ``repr_format`` value if defined and if not to the first + format available in the format registry:: + + >>> class Numeric(mkDataSpace(fr, cr, string_format=Hex)): pass + >>> str(Numeric(12)) + '0xc' + + + Internal Format + =============== + + ``DataSpace`` instance have an internal format which is the format + that is effectively used to store the value. This format may be fixed + of variable. By default it is variable, and this means that the value + stored can change format any time. + + In some case you might want to use a fixed format to store your value. + + This will not change any visible behavior, the value being converted + back and forth to the expected format anyway:: + + >>> class Numeric(mkDataSpace(fr, cr, internal_format=Hex)): pass + >>> Numeric(1) + + >>> Numeric(1).hex + '0x1' + + In our example, we only have a path to convert from ``Dec`` to ``Bin`` and + not the reverse:: + + >>> class Numeric(mkDataSpace(fr, cr, internal_format=Bin)): pass + >>> x = Numeric(15) + >>> x.bin + '0b1111' + + Instanciation, attribute acces was okay, but:: + + >>> x + Traceback (most recent call last): + ... + ValueError: No convertion path found from bin to dec format. + + Because representaiton of the object should be in ``Dec`` format. + + + Attribute + ========= + + There are 2 different type of magic attribute usable on + ``DataSpace`` instances: + - attributes that uses the format names and aims at setting + or getting conversion in other formats. + - attributes that uses subcomponent name of namedtuple + + Format's name + ------------- + + Each ``DataSpace`` instance will provide attributes following the name + of every format of the format registry that is reachable thanks to the + converters. + + >>> class Numeric(mkDataSpace(fr, cr)): pass + >>> Numeric(1).hex + '0x1' + >>> Numeric(1).bin + '0b1' + + These attribute are read/write, so you can set the value of the instance + easily:: + + >>> x = Numeric(12) + >>> x + + >>> x.hex = '0x12' + >>> x + + >>> x.bin = '0b10101' + + We didn't provide any way to convert from binary to dec, which is + the representation format here, so: + + >>> x + Traceback (most recent call last): + ... + ValueError: No convertion path found from bin to dec format. + + Notice that this only makes an error upon usage, because the + internal format (see section) is not fixed, and will thus follow + blindly the last attribute assignation's format. + + Of course, referring to an attribute label that can't be infered + following the above rules then it'll cast an attribute error:: + + >>> class Numeric(mkDataSpace(fr, cr)): pass + >>> Numeric(1).foo + Traceback (most recent call last): + ... + AttributeError: 'foo' not found + + You can't read it and can't set it:: + + >>> Numeric(1).foo = 2 + Traceback (most recent call last): + ... + AttributeError: foo + + + Edge cases + ========== + + If you provide an empty format registry, it'll complain: + + >>> mkDataSpace(FormatRegistry(), ConverterRegistry(), None) + Traceback (most recent call last): + ... + ValueError: formats registry provided is empty. + + """ + + if len(formats) == 0: + raise ValueError("formats registry provided is empty.") + + ## defaults + repr_format = repr_format or formats[0] + string_format = string_format or repr_format + input_formats = input_formats or [repr_format] + + class DataSpace(object): + """Relative Element in multi-representation data + + This object holds an internal representation of a data in a + format (fixed or variable) and provide means to translate the + data in available formats thanks to a set of converter functions. + + """ + + _internal = None + + def __init__(self, value=None, pick_for=None, pick_key=hash_or_str, + picker=None, **kwargs): + + if pick_key is None: + pick_key = lambda x: x + + if pick_for is not None: + if not (picker or self._picker): + raise ValueError( + "Can't pick value as no picker was defined.") + value = (picker or self._picker)(pick_key(pick_for)) + + if isinstance(value, DataSpace): + value_if_name = str(type(value._internal)) + setattr(self, value_if_name, + getattr(value, value_if_name)) + elif isinstance(value, tuple(formats)): + self._internal = value.convert(self._internal_format, converters) \ + if self._internal_format else value + else: + for f in input_formats: + try: + setattr(self, str(f), value) + break + except (ValueError, TypeError): + continue + else: + ## maybe keyword values were used + if len(set(kwargs.keys()) & + set(str(f) for f in self._formats)) == 0: + raise ValueError( + "No input formats are able to read %r (tried %s)" + % (value, + ", ".join(f.__name__ for f in input_formats))) + + self.equality = RGB_equivalence + + for k, v in kwargs.items(): + setattr(self, k, v) + + def __getattr__(self, label): + f, attr = self._formats.find(label) + if f is not None: + if attr is not None: + return getattr( + getattr(self, str(f)), + attr) + return self._internal.convert(f, self._converters) + raise AttributeError("'%s' not found" % label) + + def __setattr__(self, label, value): + if label.startswith("_") or label == "equality": + self.__dict__[label] = value + return + + f, attr = self._formats.find(label) + if f is None: + raise AttributeError(label) + elif attr is None: + if not isinstance(value, f): + try: + value = f(value) + except: + msg = format_last_exception() + raise ValueError( + "Instantiation of %s failed with given value %s.\n%s" + % (type(value).__name__, value, msg)) + if self._internal_format: + value = value.convert(self._internal_format, + self._converters) + self._internal = value + else: ## attr is not None + setattr(self, str(f), + getattr(self, str(f))._replace(**{attr: value})) + + def __str__(self): + return "%s" % (getattr(self, str(self._string_format)), ) + + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, + getattr(self, str(self._repr_format))) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.equality(self, other) + return NotImplemented + + if sys.version_info[0] == 2: + ## Note: intended to be a backport of python 3 behavior + def __ne__(self, other): + equal = self.__eq__(other) + return equal if equal is NotImplemented else not equal + + frame = inspect.currentframe() + argvalues = inspect.getargvalues(frame) + for k in argvalues.args: + value = argvalues.locals[k] + if k == "picker" and value: + value = staticmethod(value) + setattr(DataSpace, "_%s" % k, value) + return DataSpace +## +## Color equivalence +## -class C_HSL: +RGB_equivalence = lambda c1, c2: c1.hex == c2.hex +HSL_equivalence = lambda c1, c2: c1.hsl == c2.hsl - def __getattr__(self, value): - label = value.lower() - if label in COLOR_NAME_TO_RGB: - return rgb2hsl(tuple(v / 255. for v in COLOR_NAME_TO_RGB[label])) - raise AttributeError("%s instance has no attribute %r" - % (self.__class__, value)) +## +## Module wide color object +## -HSL = C_HSL() +def make_color_factory(**kwargs_defaults): + + def ColorFactory(*args, **kwargs): + new_kwargs = kwargs_defaults.copy() + new_kwargs.update(kwargs) + return Color(*args, **new_kwargs) + return ColorFactory -class C_RGB: - """RGB colors container - Provides a quick color access. +## +## Convenience +## - >>> from colour import RGB +def format_last_exception(prefix=" | "): + """Format the last exception for display it in tests. - >>> RGB.WHITE - Rgb(red=1.0, green=1.0, blue=1.0) - >>> RGB.BLUE - Rgb(red=0.0, green=0.0, blue=1.0) + This allows to raise custom exception, without loosing the context of what + caused the problem in the first place: - >>> RGB.DONOTEXISTS # doctest: +ELLIPSIS + >>> def f(): + ... raise Exception("Something terrible happened") + >>> try: ## doctest: +ELLIPSIS + ... f() + ... except Exception: + ... formated_exception = format_last_exception() + ... raise ValueError('Oups, an error occured:\\n%s' + ... % formated_exception) Traceback (most recent call last): ... - AttributeError: ... has no attribute 'DONOTEXISTS' + ValueError: Oups, an error occured: + | Traceback (most recent call last): + ... + | Exception: Something terrible happened """ - def __getattr__(self, value): - return hsl2rgb(getattr(HSL, value)) + return '\n'.join( + str(prefix + line) + for line in traceback.format_exc().strip().split('\n')) -class C_HEX: - """RGB colors container +## +## Color Formats +## - Provides a quick color access. +## Global module wide registry +Formats = FormatRegistry() - >>> from colour import HEX +@register_format(Formats) +class Web(String): + """string object with english color names or use short or long hex repr - >>> HEX.WHITE - '#fff' - >>> HEX.BLUE - '#00f' + This format is used most notably in HTML/CSS for its ease of use. - >>> HEX.DONOTEXISTS # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - AttributeError: ... has no attribute 'DONOTEXISTS' + ex: 'red', '#123456', '#fff' are all web representation. + + >>> Web('white') + 'white' + >>> Web.white + 'white' + + >>> Web('foo') + Traceback (most recent call last): + ... + ValueError: 'foo' is not a recognized color for Web format. + + >>> Web('#foo') + Traceback (most recent call last): + ... + ValueError: Invalid hex string specifier '#foo' for Web format + + Web has a default value to 'blue':: + + >>> Web(None) + 'blue' """ - def __getattr__(self, value): - return rgb2hex(getattr(RGB, value)) + default = 'blue' -RGB = C_RGB() -HEX = C_HEX() + @classmethod + def _validate(cls, s): + if s.startswith('#'): + if not HexS.regex.match(s): + raise ValueError( + "Invalid hex string specifier '%s' for Web format" + % s) + return s + web = s.lower() + if web not in COLOR_NAME_TO_RGB: + raise ValueError( + "%r is not a recognized color for Web format." + % web) + return web -## -## Types -## +@register_format(Formats) +class HSL(Tuple("hue", "saturation", "luminance")): + """3-uple of Hue, Saturation, Lightness all between 0.0 and 1.0 -Hsl = collections.namedtuple("Hsl", ["hue", "saturation", "luminance"]) -Rgb = collections.namedtuple("Rgb", ["red", "green", "blue"]) + As all ``Format`` subclass, it can instantiate color based on the X11 + color names:: + >>> HSL.white + HSL(hue=0.0, saturation=0.0, luminance=1.0) -def format_get(label, formats): - for f in formats: - if label == format_id(f): - return f - return None + """ -def format_id(f): - if isinstance(f, type): - return f.__name__.lower() - else: - return f +@register_format(Formats) +class RGB(Tuple("red", "green", "blue")): + """3-uple of Red, Green, Blue all values between 0.0 and 1.0 + As all ``Format`` subclass, it can instantiate color based on the X11 + color names:: -def format_get_by_attr(label, formats): - for f in formats: - for attr in getattr(f, "_fields", []): - if label == attr: - return f - return None + >>> RGB.darkcyan ## doctest: +ELLIPSIS + RGB(red=0.0, green=0.545..., blue=0.545...) + >>> RGB.WHITE + RGB(red=1.0, green=1.0, blue=1.0) + >>> RGB.BLUE + RGB(red=0.0, green=0.0, blue=1.0) + + >>> RGB.DOESNOTEXIST # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AttributeError: ... has no attribute 'DOESNOTEXIST' + + """ + + +@register_format(Formats) +class Hex(String): + """7-chars string starting with '#' and with red, green, blue values + Each color is expressed in 2 hex digit each. + + Note: This format accept only 6-dex digit + + Example: '#ffffff' + + As all ``Format`` subclass, it can instantiate color based on the X11 + color names:: + + >>> Hex.WHITE + '#ffffff' + >>> type(Hex.WHITE) + + >>> Hex.BLUE + '#0000ff' + + """ + + regex = re.compile(r'^#[0-9a-fA-F]{6}$') + + +@register_format(Formats) +class HexS(String): + """string starting with '#' and with red, green, blue values + + This format accept color in 3 or 6 value ex: '#fff' or '#ffffff' + + """ + + regex = re.compile(r'^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$') -def format_find(label, formats): - f = format_get(label, formats) - if f is not None: - return f, None - return format_get_by_attr(label, formats), label ## -## Convertion function +## Converters ## + +## Module wide converters +Converters = ConverterRegistry() + + +@register_converter(Converters, HSL, RGB) def hsl2rgb(hsl): """Convert HSL representation towards RGB @@ -331,49 +1213,51 @@ def hsl2rgb(hsl): With a lightness put at 0, RGB is always rgbblack >>> hsl2rgb((0.0, 0.0, 0.0)) - Rgb(red=0.0, green=0.0, blue=0.0) + (0.0, 0.0, 0.0) >>> hsl2rgb((0.5, 0.0, 0.0)) - Rgb(red=0.0, green=0.0, blue=0.0) + (0.0, 0.0, 0.0) >>> hsl2rgb((0.5, 0.5, 0.0)) - Rgb(red=0.0, green=0.0, blue=0.0) + (0.0, 0.0, 0.0) Same for lightness put at 1, RGB is always rgbwhite >>> hsl2rgb((0.0, 0.0, 1.0)) - Rgb(red=1.0, green=1.0, blue=1.0) + (1.0, 1.0, 1.0) >>> hsl2rgb((0.5, 0.0, 1.0)) - Rgb(red=1.0, green=1.0, blue=1.0) + (1.0, 1.0, 1.0) >>> hsl2rgb((0.5, 0.5, 1.0)) - Rgb(red=1.0, green=1.0, blue=1.0) + (1.0, 1.0, 1.0) With saturation put at 0, the RGB should be equal to Lightness: >>> hsl2rgb((0.0, 0.0, 0.25)) - Rgb(red=0.25, green=0.25, blue=0.25) + (0.25, 0.25, 0.25) >>> hsl2rgb((0.5, 0.0, 0.5)) - Rgb(red=0.5, green=0.5, blue=0.5) + (0.5, 0.5, 0.5) >>> hsl2rgb((0.5, 0.0, 0.75)) - Rgb(red=0.75, green=0.75, blue=0.75) + (0.75, 0.75, 0.75) With saturation put at 1, and lightness put to 0.5, we can find normal full red, green, blue colors: >>> hsl2rgb((0 , 1.0, 0.5)) - Rgb(red=1.0, green=0.0, blue=0.0) + (1.0, 0.0, 0.0) >>> hsl2rgb((1 , 1.0, 0.5)) - Rgb(red=1.0, green=0.0, blue=0.0) + (1.0, 0.0, 0.0) >>> hsl2rgb((1.0/3 , 1.0, 0.5)) - Rgb(red=0.0, green=1.0, blue=0.0) + (0.0, 1.0, 0.0) >>> hsl2rgb((2.0/3 , 1.0, 0.5)) - Rgb(red=0.0, green=0.0, blue=1.0) + (0.0, 0.0, 1.0) Of course: + >>> hsl2rgb((0.0, 2.0, 0.5)) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: Saturation must be between 0 and 1. And: + >>> hsl2rgb((0.0, 0.0, 1.5)) # doctest: +ELLIPSIS Traceback (most recent call last): ... @@ -388,7 +1272,7 @@ def hsl2rgb(hsl): raise ValueError("Lightness must be between 0 and 1.") if s == 0: - return Rgb(l, l, l) + return l, l, l if l < 0.5: v2 = l * (1.0 + s) @@ -401,9 +1285,10 @@ def hsl2rgb(hsl): g = _hue2rgb(v1, v2, h) b = _hue2rgb(v1, v2, h - (1.0 / 3)) - return Rgb(r, g, b) + return r, g, b +@register_converter(Converters, RGB, HSL) def rgb2hsl(rgb): """Convert RGB representation towards HSL @@ -424,35 +1309,35 @@ def rgb2hsl(rgb): >>> rgb2hsl((1.0, 1.0, 1.0)) # doctest: +ELLIPSIS - Hsl(hue=..., saturation=0.0, luminance=1.0) + (..., 0.0, 1.0) >>> rgb2hsl((0.5, 0.5, 0.5)) # doctest: +ELLIPSIS - Hsl(hue=..., saturation=0.0, luminance=0.5) + (..., 0.0, 0.5) >>> rgb2hsl((0.0, 0.0, 0.0)) # doctest: +ELLIPSIS - Hsl(hue=..., saturation=0.0, luminance=0.0) + (..., 0.0, 0.0) If only one color is different from the others, it defines the direct Hue: >>> rgb2hsl((0.5, 0.5, 1.0)) # doctest: +ELLIPSIS - Hsl(hue=0.66..., saturation=1.0, luminance=0.75) + (0.66..., 1.0, 0.75) >>> rgb2hsl((0.2, 0.1, 0.1)) # doctest: +ELLIPSIS - Hsl(hue=0.0, saturation=0.33..., luminance=0.15...) + (0.0, 0.33..., 0.15...) Having only one value set, you can check that: >>> rgb2hsl((1.0, 0.0, 0.0)) - Hsl(hue=0.0, saturation=1.0, luminance=0.5) + (0.0, 1.0, 0.5) >>> rgb2hsl((0.0, 1.0, 0.0)) # doctest: +ELLIPSIS - Hsl(hue=0.33..., saturation=1.0, luminance=0.5) + (0.33..., 1.0, 0.5) >>> rgb2hsl((0.0, 0.0, 1.0)) # doctest: +ELLIPSIS - Hsl(hue=0.66..., saturation=1.0, luminance=0.5) + (0.66..., 1.0, 0.5) Regression check upon very close values in every component of red, green and blue: >>> rgb2hsl((0.9999999999999999, 1.0, 0.9999999999999994)) ... ## doctest: +ELLIPSIS - Hsl(hue=0.0, saturation=0.0, luminance=0.999...) + (0.0, 0.0, 0.999...) Of course: @@ -484,7 +1369,7 @@ def rgb2hsl(rgb): l = vsum / 2 if diff < FLOAT_ERROR: ## This is a gray, no chroma... - return Hsl(0.0, 0.0, l) + return 0.0, 0.0, l ## ## Chromatic data... @@ -510,7 +1395,7 @@ def rgb2hsl(rgb): if h < 0: h += 1 if h > 1: h -= 1 - return Hsl(h, s, l) + return h, s, l def _hue2rgb(v1, v2, vH): @@ -530,23 +1415,24 @@ def _hue2rgb(v1, v2, vH): return v1 -def hexl2hexs(hex): +@register_converter(Converters, Hex, HexS) +def hex2hexs(hex): """Shorten from 6 to 3 hex represention if possible Usage ----- - >>> from colour import rgb2hex + >>> from colour import hex2hexs Provided a long string hex format, it should shorten it when possible:: - >>> hexl2hexs('#00ff00') + >>> hex2hexs('#00ff00') '#0f0' In the following case, it is not possible to shorten, thus:: - >>> hexl2hexs('#01ff00') + >>> hex2hexs('#01ff00') '#01ff00' """ @@ -556,7 +1442,33 @@ def hexl2hexs(hex): return hex -def rgb2hex(rgb, force_long=False): +@register_converter(Converters, HexS, Hex) +def hexs2hex(hex): + """Enlarge possible short 3 hexgit to give full hex 6 char long + + Usage + ----- + + >>> from colour import hexs2hex + + Provided a short string hex format, it should enlarge it:: + + >>> hexs2hex('#0f0') + '#00ff00' + + In the following case, it is already enlargened, thus:: + + >>> hexs2hex('#01ff00') + '#01ff00' + + """ + if not Hex.regex.match(hex): + return '#' + ''.join([("%s" % (t, )) * 2 for t in hex[1:]]) + return hex + + +@register_converter(Converters, RGB, Hex) +def rgb2hex(rgb): """Transform RGB tuple to hex RGB representation :param rgb: RGB 3-uple of float between 0 and 1 @@ -567,29 +1479,24 @@ def rgb2hex(rgb, force_long=False): >>> from colour import rgb2hex - >>> rgb2hex((0.0,1.0,0.0)) - '#0f0' + >>> rgb2hex((0.0, 1.0, 0.0)) + '#00ff00' Rounding try to be as natural as possible: - >>> rgb2hex((0.0,0.999999,1.0)) - '#0ff' - - And if not possible, the 6 hex char representation is used: - - >>> rgb2hex((0.23,1.0,1.0)) - '#3bffff' - - >>> rgb2hex((0.0,0.999999,1.0), force_long=True) + >>> rgb2hex((0.0, 0.999999, 1.0)) '#00ffff' + >>> rgb2hex((0.5, 0.999999, 1.0)) + '#7fffff' + """ - hx = "#" + ''.join(["%02x" % int(c * 255 + 0.5 - FLOAT_ERROR) - for c in rgb]) - return hx if force_long else hexl2hexs(hx) + return "#" + ''.join(["%02x" % int(c * 255 + 0.5 - FLOAT_ERROR) + for c in rgb]) +@register_converter(Converters, Hex, RGB) def hex2rgb(str_rgb): """Transform hex RGB representation to RGB tuple @@ -599,13 +1506,13 @@ def hex2rgb(str_rgb): >>> from colour import hex2rgb >>> hex2rgb('#00ff00') - Rgb(red=0.0, green=1.0, blue=0.0) + (0.0, 1.0, 0.0) >>> hex2rgb('#0f0') - Rgb(red=0.0, green=1.0, blue=0.0) + (0.0, 1.0, 0.0) >>> hex2rgb('#aaa') # doctest: +ELLIPSIS - Rgb(red=0.66..., green=0.66..., blue=0.66...) + (0.66..., 0.66..., 0.66...) >>> hex2rgb('#aa') # doctest: +ELLIPSIS Traceback (most recent call last): @@ -628,16 +1535,17 @@ def hex2rgb(str_rgb): "Invalid value %r provided as hex color for rgb conversion." % str_rgb) - return Rgb(*[float(int(v, 16)) / 255 for v in (r, g, b)]) + return tuple(float(int(v, 16)) / 255 for v in (r, g, b)) +@register_converter(Converters, Hex, Web) def hex2web(hex): - """Converts HEX representation to WEB + """Converts Hex representation to Web :param rgb: 3 hex char or 6 hex char string representation :rtype: web string representation (human readable if possible) - WEB representation uses X11 rgb.txt to define convertion + Web representation uses X11 rgb.txt to define convertion between RGB and english color names. Usage @@ -651,9 +1559,6 @@ def hex2web(hex): >>> hex2web('#aaaaaa') '#aaa' - >>> hex2web('#abc') - '#abc' - >>> hex2web('#acacac') '#acacac' @@ -666,17 +1571,22 @@ def hex2web(hex): return color_name if len(re.sub(r"[^A-Z]", "", color_name)) > 1 \ else color_name.lower() - return hexl2hexs(hex) + return hex2hexs(hex) -def web2hex(web, force_long=False): - """Converts WEB representation to HEX +@register_converter(Converters, Web, Hex) +def web2hex(web): + """Converts Web representation to Hex :param rgb: web string representation (human readable if possible) :rtype: 3 hex char or 6 hex char string representation - WEB representation uses X11 rgb.txt to define convertion - between RGB and english color names. + Web representation uses X11 rgb.txt (converted in array in this file) + to define convertion between RGB and english color names. + + As of https://www.w3.org/TR/css3-color/#svg-color, there are 147 names + recognized. + Usage ===== @@ -684,19 +1594,16 @@ def web2hex(web, force_long=False): >>> from colour import web2hex >>> web2hex('red') - '#f00' + '#ff0000' >>> web2hex('#aaa') - '#aaa' + '#aaaaaa' >>> web2hex('#foo') # doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: '#foo' is not in web format. Need 3 or 6 hex digit. - >>> web2hex('#aaa', force_long=True) - '#aaaaaa' - >>> web2hex('#aaaaaa') '#aaaaaa' @@ -717,11 +1624,10 @@ def web2hex(web, force_long=False): """ if web.startswith('#'): - if (LONG_HEX_COLOR.match(web) or - (not force_long and SHORT_HEX_COLOR.match(web))): + if Hex.regex.match(web): return web.lower() - elif SHORT_HEX_COLOR.match(web) and force_long: - return '#' + ''.join([("%s" % (t, )) * 2 for t in web[1:]]) + elif HexS.regex.match(web): + return hexs2hex(web) raise AttributeError( "%r is not in web format. Need 3 or 6 hex digit." % web) @@ -729,129 +1635,16 @@ def web2hex(web, force_long=False): if web not in COLOR_NAME_TO_RGB: raise ValueError("%r is not a recognized color." % web) - return rgb2hex([float(int(v)) / 255 for v in COLOR_NAME_TO_RGB[web]], - force_long) - - -## Missing functions convertion - -hsl2hex = lambda x: rgb2hex(hsl2rgb(x)) -hex2hsl = lambda x: rgb2hsl(hex2rgb(x)) -rgb2web = lambda x: hex2web(rgb2hex(x)) -web2rgb = lambda x: hex2rgb(web2hex(x)) -web2hsl = lambda x: rgb2hsl(web2rgb(x)) -hsl2web = lambda x: rgb2web(hsl2rgb(x)) - - -def color_scale(begin_hsl, end_hsl, nb): - """Returns a list of nb color HSL tuples between begin_hsl and end_hsl - - >>> from colour import color_scale - - >>> [rgb2hex(hsl2rgb(hsl)) for hsl in color_scale((0, 1, 0.5), - ... (1, 1, 0.5), 3)] - ['#f00', '#0f0', '#00f', '#f00'] - - >>> [rgb2hex(hsl2rgb(hsl)) - ... for hsl in color_scale((0, 0, 0), - ... (0, 0, 1), - ... 15)] # doctest: +ELLIPSIS - ['#000', '#111', '#222', ..., '#ccc', '#ddd', '#eee', '#fff'] - - Of course, asking for negative values is not supported: - - >>> color_scale((0, 1, 0.5), (1, 1, 0.5), -2) - Traceback (most recent call last): - ... - ValueError: Unsupported negative number of colors (nb=-2). - - """ - - if nb < 0: - raise ValueError( - "Unsupported negative number of colors (nb=%r)." % nb) - - step = tuple([float(end_hsl[i] - begin_hsl[i]) / nb for i in range(0, 3)]) \ - if nb > 0 else (0, 0, 0) - - def mul(step, value): - return tuple([v * value for v in step]) - - def add_v(step, step2): - return tuple([v + step2[i] for i, v in enumerate(step)]) - - return [add_v(begin_hsl, mul(step, r)) for r in range(0, nb + 1)] - - -## -## Color Pickers -## - -def RGB_color_picker(obj): - """Build a color representation from the string representation of an object - - This allows to quickly get a color from some data, with the - additional benefit that the color will be the same as long as the - (string representation of the) data is the same:: - - >>> from colour import RGB_color_picker, Color - - Same inputs produce the same result:: - - >>> RGB_color_picker("Something") == RGB_color_picker("Something") - True - - ... but different inputs produce different colors:: - - >>> RGB_color_picker("Something") != RGB_color_picker("Something else") - True - - In any case, we still get a ``Color`` object:: - - >>> isinstance(RGB_color_picker("Something"), Color) - True - - """ - - ## Turn the input into a by 3-dividable string. SHA-384 is good because it - ## divides into 3 components of the same size, which will be used to - ## represent the RGB values of the color. - digest = hashlib.sha384(str(obj).encode('utf-8')).hexdigest() - - ## Split the digest into 3 sub-strings of equivalent size. - subsize = int(len(digest) / 3) - splitted_digest = [digest[i * subsize: (i + 1) * subsize] - for i in range(3)] - - ## Convert those hexadecimal sub-strings into integer and scale them down - ## to the 0..1 range. - max_value = float(int("f" * subsize, 16)) - components = ( - int(d, 16) ## Make a number from a list with hex digits - / max_value ## Scale it down to [0.0, 1.0] - for d in splitted_digest) - - return Color(rgb2hex(components)) ## Profit! - - -def hash_or_str(obj): - try: - return hash((type(obj).__name__, obj)) - except TypeError: - ## Adds the type name to make sure two object of different type but - ## identical string representation get distinguished. - return "\0".join([type(obj).__name__, str(obj)]) - + return rgb2hex([float(int(v)) / 255 for v in COLOR_NAME_TO_RGB[web]]) -## -## All purpose object -## -class Color(object): +class Color(mkDataSpace(formats=Formats, converters=Converters, + picker=RGB_color_picker)): """Abstraction of a color object - Color object keeps information of a color. It can input/output to different - format (HSL, RGB, HEX, WEB) and their partial representation. + Color object keeps information of a color. It can input/output to + different formats (the ones registered in Formats object) and + their partial representation. >>> from colour import Color, HSL @@ -876,11 +1669,11 @@ class Color(object): 0.0 >>> b.rgb - Rgb(red=0.0, green=0.0, blue=1.0) + RGB(red=0.0, green=0.0, blue=1.0) >>> b.hsl # doctest: +ELLIPSIS - Hsl(hue=0.66..., saturation=1.0, luminance=0.5) + HSL(hue=0.66..., saturation=1.0, luminance=0.5) >>> b.hex - '#00f' + '#0000ff' Change values ------------- @@ -889,30 +1682,30 @@ class Color(object): >>> b.hue = 0.0 >>> b.hex - '#f00' + '#ff0000' >>> b.hue = 2.0/3 >>> b.hex - '#00f' + '#0000ff' In the other way round: - >>> b.hex = '#f00' + >>> b.hexs = '#f00' >>> b.hsl - Hsl(hue=0.0, saturation=1.0, luminance=0.5) + HSL(hue=0.0, saturation=1.0, luminance=0.5) Long hex can be accessed directly: - >>> b.hex_l = '#123456' - >>> b.hex_l - '#123456' + >>> b.hex = '#123456' >>> b.hex '#123456' + >>> b.hexs + '#123456' - >>> b.hex_l = '#ff0000' - >>> b.hex_l - '#ff0000' + >>> b.hex = '#ff0000' >>> b.hex + '#ff0000' + >>> b.hexs '#f00' Convenience @@ -927,9 +1720,9 @@ class Color(object): >>> c.saturation = 0.0 >>> c.hsl # doctest: +ELLIPSIS - Hsl(hue=..., saturation=0.0, luminance=0.5) + HSL(hue=..., saturation=0.0, luminance=0.5) >>> c.rgb - Rgb(red=0.5, green=0.5, blue=0.5) + RGB(red=0.5, green=0.5, blue=0.5) >>> c.hex '#7f7f7f' >>> c @@ -940,15 +1733,21 @@ class Color(object): >>> c.hex - '#000' + '#000000' >>> c.green = 1.0 >>> c.blue = 1.0 >>> c.hex - '#0ff' + '#00ffff' >>> c + Equivalently, in one go: + + >>> c.rgb = (1, 1, 0) + >>> c + + >>> c = Color('blue', luminance=0.75) >>> c @@ -1019,136 +1818,11 @@ class Color(object): And keep the internal API working:: >>> Tint("red").hsl - Hsl(hue=0.0, saturation=1.0, luminance=0.5) + HSL(hue=0.0, saturation=1.0, luminance=0.5) """ - _hsl = None ## internal representation - _FORMATS = [Rgb, Hsl, "hex", "web"] - - def __init__(self, color=None, - pick_for=None, picker=RGB_color_picker, pick_key=hash_or_str, - **kwargs): - - if pick_key is None: - pick_key = lambda x: x - - if pick_for is not None: - color = picker(pick_key(pick_for)) - - if isinstance(color, Color): - self.web = color.web - else: - self.web = color if color else 'black' - - self.equality = RGB_equivalence - - for k, v in kwargs.items(): - setattr(self, k, v) - - def __getattr__(self, label): - if label.startswith("get_"): - l = label[4:] - f, attr = format_find(l, self._FORMATS) - if f is not None: - current_format = Hsl - if attr is not None: - return lambda: getattr( - getattr(self, format_id(f)), - attr) - function_label = "%s2%s" % (format_id(current_format), - format_id(f)) - fun = globals().get(function_label) - if fun: - return lambda: fun( - getattr(self, format_id(current_format))) - raise AttributeError("'%s' not found" % label) - try: - return getattr(self, 'get_' + label)() - except AttributeError: - raise AttributeError("'%s' not found" % label) - - def __setattr__(self, label, value): - if label in ["_hsl", "equality"]: - self.__dict__[label] = value - return - - method_label = 'set_' + label - if hasattr(self, method_label): - fc = getattr(self, method_label) - fc(value) - return - f, attr = format_find(label, self._FORMATS) - if f is not None: - current_format = Hsl - if attr is not None: - setattr(self, format_id(f), - getattr(self, format_id(f))._replace(**{attr: value})) - return - function_label = "%s2%s" % (format_id(f), - format_id(current_format)) - fun = globals().get(function_label) - if fun: - setattr(self, format_id(current_format), fun(value)) - return - - raise AttributeError(label) - - ## - ## Get - ## - - def get_hsl(self): - return Hsl(*self._hsl) - - def get_hex_l(self): - return rgb2hex(self.rgb, force_long=True) - - ## - ## Set - ## - - def set_hsl(self, value): - self._hsl = list(value) - - set_hex_l = lambda s, v: setattr(s, "hex", v) - - ## range of color generation - def range_to(self, value, steps): - for hsl in color_scale(self._hsl, Color(value).hsl, steps - 1): - yield Color(hsl=hsl) - - ## - ## Convenience - ## + for hsl in color_scale(self.hsl, self.__class__(value).hsl, steps - 1): + yield self.__class__(hsl=hsl) - def __str__(self): - return "%s" % self.web - - def __repr__(self): - return "" % self.web - - def __eq__(self, other): - if isinstance(other, Color): - return self.equality(self, other) - return NotImplemented - - if sys.version_info[0] == 2: - ## Note: intended to be a backport of python 3 behavior - def __ne__(self, other): - equal = self.__eq__(other) - return equal if equal is NotImplemented else not equal - - -RGB_equivalence = lambda c1, c2: c1.hex_l == c2.hex_l -HSL_equivalence = lambda c1, c2: c1._hsl == c2._hsl - - -def make_color_factory(**kwargs_defaults): - - def ColorFactory(*args, **kwargs): - new_kwargs = kwargs_defaults.copy() - new_kwargs.update(kwargs) - return Color(*args, **new_kwargs) - return ColorFactory From c3f9d6d4138b31945750e398e34ea6f838fcaac3 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Mon, 24 Apr 2017 15:45:32 +0800 Subject: [PATCH 07/15] fix: dev: indent all doctests. !minor --- colour.py | 285 +++++++++++++++++++++++++++--------------------------- 1 file changed, 143 insertions(+), 142 deletions(-) diff --git a/colour.py b/colour.py index 4ceffed..56c70e6 100644 --- a/colour.py +++ b/colour.py @@ -491,24 +491,24 @@ def decorator(f): def color_scale(begin_hsl, end_hsl, nb): """Returns a list of nb color HSL tuples between begin_hsl and end_hsl - >>> from colour import color_scale + >>> from colour import color_scale - >>> [HSL(hsl).convert(HexS) for hsl in color_scale((0, 1, 0.5), - ... (1, 1, 0.5), 3)] - ['#f00', '#0f0', '#00f', '#f00'] + >>> [HSL(hsl).convert(HexS) for hsl in color_scale((0, 1, 0.5), + ... (1, 1, 0.5), 3)] + ['#f00', '#0f0', '#00f', '#f00'] - >>> [HSL(hsl).convert(HexS) - ... for hsl in color_scale((0, 0, 0), - ... (0, 0, 1), - ... 15)] # doctest: +ELLIPSIS - ['#000', '#111', '#222', ..., '#ccc', '#ddd', '#eee', '#fff'] + >>> [HSL(hsl).convert(HexS) + ... for hsl in color_scale((0, 0, 0), + ... (0, 0, 1), + ... 15)] # doctest: +ELLIPSIS + ['#000', '#111', '#222', ..., '#ccc', '#ddd', '#eee', '#fff'] Of course, asking for negative values is not supported: - >>> color_scale((0, 1, 0.5), (1, 1, 0.5), -2) - Traceback (most recent call last): - ... - ValueError: Unsupported negative number of colors (nb=-2). + >>> color_scale((0, 1, 0.5), (1, 1, 0.5), -2) + Traceback (most recent call last): + ... + ValueError: Unsupported negative number of colors (nb=-2). """ @@ -961,7 +961,8 @@ def __setattr__(self, label, value): except: msg = format_last_exception() raise ValueError( - "Instantiation of %s failed with given value %s.\n%s" + "Instantiation of %s failed with given value %s." + "\n%s" % (type(value).__name__, value, msg)) if self._internal_format: value = value.convert(self._internal_format, @@ -1030,20 +1031,20 @@ def format_last_exception(prefix=" | "): This allows to raise custom exception, without loosing the context of what caused the problem in the first place: - >>> def f(): - ... raise Exception("Something terrible happened") - >>> try: ## doctest: +ELLIPSIS - ... f() - ... except Exception: - ... formated_exception = format_last_exception() - ... raise ValueError('Oups, an error occured:\\n%s' - ... % formated_exception) - Traceback (most recent call last): - ... - ValueError: Oups, an error occured: - | Traceback (most recent call last): - ... - | Exception: Something terrible happened + >>> def f(): + ... raise Exception("Something terrible happened") + >>> try: ## doctest: +ELLIPSIS + ... f() + ... except Exception: + ... formated_exception = format_last_exception() + ... raise ValueError('Oups, an error occured:\\n%s' + ... % formated_exception) + Traceback (most recent call last): + ... + ValueError: Oups, an error occured: + | Traceback (most recent call last): + ... + | Exception: Something terrible happened """ @@ -1059,6 +1060,7 @@ def format_last_exception(prefix=" | "): ## Global module wide registry Formats = FormatRegistry() + @register_format(Formats) class Web(String): """string object with english color names or use short or long hex repr @@ -1208,60 +1210,60 @@ def hsl2rgb(hsl): Here are some quick notion of HSL to RGB convertion: - >>> from colour import hsl2rgb + >>> from colour import hsl2rgb With a lightness put at 0, RGB is always rgbblack - >>> hsl2rgb((0.0, 0.0, 0.0)) - (0.0, 0.0, 0.0) - >>> hsl2rgb((0.5, 0.0, 0.0)) - (0.0, 0.0, 0.0) - >>> hsl2rgb((0.5, 0.5, 0.0)) - (0.0, 0.0, 0.0) + >>> hsl2rgb((0.0, 0.0, 0.0)) + (0.0, 0.0, 0.0) + >>> hsl2rgb((0.5, 0.0, 0.0)) + (0.0, 0.0, 0.0) + >>> hsl2rgb((0.5, 0.5, 0.0)) + (0.0, 0.0, 0.0) Same for lightness put at 1, RGB is always rgbwhite - >>> hsl2rgb((0.0, 0.0, 1.0)) - (1.0, 1.0, 1.0) - >>> hsl2rgb((0.5, 0.0, 1.0)) - (1.0, 1.0, 1.0) - >>> hsl2rgb((0.5, 0.5, 1.0)) - (1.0, 1.0, 1.0) + >>> hsl2rgb((0.0, 0.0, 1.0)) + (1.0, 1.0, 1.0) + >>> hsl2rgb((0.5, 0.0, 1.0)) + (1.0, 1.0, 1.0) + >>> hsl2rgb((0.5, 0.5, 1.0)) + (1.0, 1.0, 1.0) With saturation put at 0, the RGB should be equal to Lightness: - >>> hsl2rgb((0.0, 0.0, 0.25)) - (0.25, 0.25, 0.25) - >>> hsl2rgb((0.5, 0.0, 0.5)) - (0.5, 0.5, 0.5) - >>> hsl2rgb((0.5, 0.0, 0.75)) - (0.75, 0.75, 0.75) + >>> hsl2rgb((0.0, 0.0, 0.25)) + (0.25, 0.25, 0.25) + >>> hsl2rgb((0.5, 0.0, 0.5)) + (0.5, 0.5, 0.5) + >>> hsl2rgb((0.5, 0.0, 0.75)) + (0.75, 0.75, 0.75) With saturation put at 1, and lightness put to 0.5, we can find normal full red, green, blue colors: - >>> hsl2rgb((0 , 1.0, 0.5)) - (1.0, 0.0, 0.0) - >>> hsl2rgb((1 , 1.0, 0.5)) - (1.0, 0.0, 0.0) - >>> hsl2rgb((1.0/3 , 1.0, 0.5)) - (0.0, 1.0, 0.0) - >>> hsl2rgb((2.0/3 , 1.0, 0.5)) - (0.0, 0.0, 1.0) + >>> hsl2rgb((0 , 1.0, 0.5)) + (1.0, 0.0, 0.0) + >>> hsl2rgb((1 , 1.0, 0.5)) + (1.0, 0.0, 0.0) + >>> hsl2rgb((1.0/3 , 1.0, 0.5)) + (0.0, 1.0, 0.0) + >>> hsl2rgb((2.0/3 , 1.0, 0.5)) + (0.0, 0.0, 1.0) Of course: - >>> hsl2rgb((0.0, 2.0, 0.5)) # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - ValueError: Saturation must be between 0 and 1. + >>> hsl2rgb((0.0, 2.0, 0.5)) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Saturation must be between 0 and 1. And: - >>> hsl2rgb((0.0, 0.0, 1.5)) # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - ValueError: Lightness must be between 0 and 1. + >>> hsl2rgb((0.0, 0.0, 1.5)) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Lightness must be between 0 and 1. """ h, s, l = [float(v) for v in hsl] @@ -1302,55 +1304,55 @@ def rgb2hsl(rgb): Here are some quick notion of RGB to HSL convertion: - >>> from colour import rgb2hsl + >>> from colour import rgb2hsl Note that if red amount is equal to green and blue, then you should have a gray value (from black to white). - - >>> rgb2hsl((1.0, 1.0, 1.0)) # doctest: +ELLIPSIS - (..., 0.0, 1.0) - >>> rgb2hsl((0.5, 0.5, 0.5)) # doctest: +ELLIPSIS - (..., 0.0, 0.5) - >>> rgb2hsl((0.0, 0.0, 0.0)) # doctest: +ELLIPSIS - (..., 0.0, 0.0) + >>> rgb2hsl((1.0, 1.0, 1.0)) # doctest: +ELLIPSIS + (..., 0.0, 1.0) + >>> rgb2hsl((0.5, 0.5, 0.5)) # doctest: +ELLIPSIS + (..., 0.0, 0.5) + >>> rgb2hsl((0.0, 0.0, 0.0)) # doctest: +ELLIPSIS + (..., 0.0, 0.0) If only one color is different from the others, it defines the direct Hue: - >>> rgb2hsl((0.5, 0.5, 1.0)) # doctest: +ELLIPSIS - (0.66..., 1.0, 0.75) - >>> rgb2hsl((0.2, 0.1, 0.1)) # doctest: +ELLIPSIS - (0.0, 0.33..., 0.15...) + >>> rgb2hsl((0.5, 0.5, 1.0)) # doctest: +ELLIPSIS + (0.66..., 1.0, 0.75) + >>> rgb2hsl((0.2, 0.1, 0.1)) # doctest: +ELLIPSIS + (0.0, 0.33..., 0.15...) Having only one value set, you can check that: - >>> rgb2hsl((1.0, 0.0, 0.0)) - (0.0, 1.0, 0.5) - >>> rgb2hsl((0.0, 1.0, 0.0)) # doctest: +ELLIPSIS - (0.33..., 1.0, 0.5) - >>> rgb2hsl((0.0, 0.0, 1.0)) # doctest: +ELLIPSIS - (0.66..., 1.0, 0.5) + >>> rgb2hsl((1.0, 0.0, 0.0)) + (0.0, 1.0, 0.5) + >>> rgb2hsl((0.0, 1.0, 0.0)) # doctest: +ELLIPSIS + (0.33..., 1.0, 0.5) + >>> rgb2hsl((0.0, 0.0, 1.0)) # doctest: +ELLIPSIS + (0.66..., 1.0, 0.5) Regression check upon very close values in every component of red, green and blue: - >>> rgb2hsl((0.9999999999999999, 1.0, 0.9999999999999994)) - ... ## doctest: +ELLIPSIS - (0.0, 0.0, 0.999...) + >>> rgb2hsl((0.9999999999999999, 1.0, 0.9999999999999994)) + ... ## doctest: +ELLIPSIS + (0.0, 0.0, 0.999...) Of course: - >>> rgb2hsl((0.0, 2.0, 0.5)) # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - ValueError: Green must be between 0 and 1. You provided 2.0. + >>> rgb2hsl((0.0, 2.0, 0.5)) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Green must be between 0 and 1. You provided 2.0. And: - >>> rgb2hsl((0.0, 0.0, 1.5)) # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - ValueError: Blue must be between 0 and 1. You provided 1.5. + + >>> rgb2hsl((0.0, 0.0, 1.5)) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Blue must be between 0 and 1. You provided 1.5. """ r, g, b = [float(v) for v in rgb] @@ -1389,7 +1391,7 @@ def rgb2hsl(rgb): h = db - dg elif g == vmax: h = (1.0 / 3) + dr - db - elif b == vmax: + else: ## b == vmax h = (2.0 / 3) + dg - dr if h < 0: h += 1 @@ -1477,18 +1479,18 @@ def rgb2hex(rgb): Usage ----- - >>> from colour import rgb2hex + >>> from colour import rgb2hex - >>> rgb2hex((0.0, 1.0, 0.0)) - '#00ff00' + >>> rgb2hex((0.0, 1.0, 0.0)) + '#00ff00' Rounding try to be as natural as possible: - >>> rgb2hex((0.0, 0.999999, 1.0)) - '#00ffff' + >>> rgb2hex((0.0, 0.999999, 1.0)) + '#00ffff' - >>> rgb2hex((0.5, 0.999999, 1.0)) - '#7fffff' + >>> rgb2hex((0.5, 0.999999, 1.0)) + '#7fffff' """ @@ -1503,23 +1505,23 @@ def hex2rgb(str_rgb): :param str_rgb: 3 hex char or 6 hex char string representation :rtype: RGB 3-uple of float between 0 and 1 - >>> from colour import hex2rgb + >>> from colour import hex2rgb - >>> hex2rgb('#00ff00') - (0.0, 1.0, 0.0) + >>> hex2rgb('#00ff00') + (0.0, 1.0, 0.0) - >>> hex2rgb('#0f0') - (0.0, 1.0, 0.0) + >>> hex2rgb('#0f0') + (0.0, 1.0, 0.0) - >>> hex2rgb('#aaa') # doctest: +ELLIPSIS - (0.66..., 0.66..., 0.66...) + >>> hex2rgb('#aaa') # doctest: +ELLIPSIS + (0.66..., 0.66..., 0.66...) - >>> hex2rgb('#aa') # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - ValueError: Invalid value '#aa' provided as hex color for rgb conversion. + >>> hex2rgb('#aa') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Invalid value '#aa' provided as hex color for rgb conversion. - """ + """ try: rgb = str_rgb[1:] @@ -1551,16 +1553,16 @@ def hex2web(hex): Usage ===== - >>> from colour import hex2web + >>> from colour import hex2web - >>> hex2web('#ff0000') - 'red' + >>> hex2web('#ff0000') + 'red' - >>> hex2web('#aaaaaa') - '#aaa' + >>> hex2web('#aaaaaa') + '#aaa' - >>> hex2web('#acacac') - '#acacac' + >>> hex2web('#acacac') + '#acacac' """ dec_rgb = tuple(int(v * 255) for v in hex2rgb(hex)) @@ -1591,36 +1593,36 @@ def web2hex(web): Usage ===== - >>> from colour import web2hex + >>> from colour import web2hex - >>> web2hex('red') - '#ff0000' + >>> web2hex('red') + '#ff0000' - >>> web2hex('#aaa') - '#aaaaaa' + >>> web2hex('#aaa') + '#aaaaaa' - >>> web2hex('#foo') # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - AttributeError: '#foo' is not in web format. Need 3 or 6 hex digit. + >>> web2hex('#foo') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AttributeError: '#foo' is not in web format. Need 3 or 6 hex digit. - >>> web2hex('#aaaaaa') - '#aaaaaa' + >>> web2hex('#aaaaaa') + '#aaaaaa' - >>> web2hex('#aaaa') # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - AttributeError: '#aaaa' is not in web format. Need 3 or 6 hex digit. + >>> web2hex('#aaaa') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AttributeError: '#aaaa' is not in web format. Need 3 or 6 hex digit. - >>> web2hex('pinky') # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - ValueError: 'pinky' is not a recognized color. + >>> web2hex('pinky') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: 'pinky' is not a recognized color. - And color names are case insensitive: + And color names are case insensitive: - >>> Color('RED') - + >>> Color('RED') + """ if web.startswith('#'): @@ -1825,4 +1827,3 @@ class Color(mkDataSpace(formats=Formats, converters=Converters, def range_to(self, value, steps): for hsl in color_scale(self.hsl, self.__class__(value).hsl, steps - 1): yield self.__class__(hsl=hsl) - From 5f1c4925e5ab51db353c06e411e80999a50f4ea8 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Mon, 24 Apr 2017 17:01:03 +0800 Subject: [PATCH 08/15] new: direct sub component access can now be more explicit by prefixing with their format name. Color instances would have to infer the target format based with the name of the attribute. For instance ``Color(..).red`` would refer to RGB format only because it is the only format being a ``namedtuple`` AND having a component named 'red'. This behavior is kept if it is not ambiguous, and it is now allowed to be more specific (to avoid ambiguous cases) by prefixing by the format name. For instance, ``.red`` is equivalent to ``.rgb_red``. --- README.rst | 8 ++++ colour.py | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index e5f0768..7456048 100644 --- a/README.rst +++ b/README.rst @@ -153,6 +153,14 @@ the different amount of red, blue, green, in the RGB format:: >>> c.green 0.0 +Here the format is inferred to be RGB (it is the only format available +having components named as these attributes), but you may want to be +more explicit, by prefixing the attribute by the format name in lower +case, so:: + + >>> c.rgb_red == c.red + True + Or the hue, saturation and luminance of the HSL representation:: >>> c.hue # doctest: +ELLIPSIS diff --git a/colour.py b/colour.py index 56c70e6..3023f46 100644 --- a/colour.py +++ b/colour.py @@ -216,14 +216,28 @@ def find(self, label): f = self.get(label, None) if f is not None: return f, None - return self.get_by_attr(label), label + return self.get_by_attr(label) def get_by_attr(self, label): - for f in self: + if "_" in label: + format, label = label.split("_", 1) + f = self.get(format, None) + formats = [] if f is None else [f] + else: + formats = list(self) + ret = [] + for f in formats: for attr in getattr(f, "_fields", []): if label == attr: - return f - return None + ret.append(f) + if len(ret) > 1: + raise ValueError( + "Ambiguous attribute %r. Try one of: %s" + % (label, ", ".join("%s_%s" % (f, label) for f in ret))) + elif len(ret) == 1: + return ret[0], label + else: ## len(ret) == 0: + return None, None def register_format(registry): @@ -848,6 +862,91 @@ def mkDataSpace(formats, converters, picker=None, internal format (see section) is not fixed, and will thus follow blindly the last attribute assignation's format. + subcomponent attribute + ---------------------- + + This is a very special feature geared toward the usage of + namedtuple formats. + + Most dataspace usage are used as reference systems translation + between multi-dimensional data. Let's take for instance + translation between polar coordinates and cartesian + coordinates:: + + >>> import math + >>> fr2 = FormatRegistry() + + >>> @register_format(fr2) + ... class Cartesian(Tuple("x", "y")): pass + >>> @register_format(fr2) + ... class Polar(Tuple("radius", "angle")): pass + + >>> cr2 = ConverterRegistry() + + >>> @register_converter(cr2, Cartesian, Polar) + ... def c2p(v): return math.sqrt(v.x**2 + v.y**2), math.atan2(v.y, v.x) + >>> @register_converter(cr2, Polar, Cartesian) + ... def p2c(p): + ... return (p.radius * math.cos(p.angle), + ... p.radius * math.sin(p.angle)) + + >>> class Point2D(mkDataSpace(fr2, cr2)): pass + + >>> point = Point2D((1, 0)) + >>> point + + + The names of the subcomponent of the tuple are directly accessible + (if there are no ambiguity):: + + >>> point.x + 1 + + >>> point.angle = math.pi + >>> point.x + -1.0 + + In case of ambiguity, you can prefix your attribute label with the + format names as such: + + >>> point.cartesian_y = 0.0 + >>> point.cartesian_x = 1.0 + >>> point.polar_angle + 0.0 + + Here is such a case: + + >>> fr3 = FormatRegistry() + + >>> @register_format(fr3) + ... class Normal(Tuple("x", "y")): pass + >>> @register_format(fr3) + ... class Inverted(Tuple("x", "y")): pass + + >>> cr3 = ConverterRegistry() + + >>> @register_converter(cr3, Normal, Inverted) + ... def n2i(v): return v.y, v.x + >>> @register_converter(cr3, Inverted, Normal) + ... def i2n(v): return v.y, v.x + + In this case, we don't expect attribute ``x`` to be found:: + + >>> class Point2D(mkDataSpace(fr3, cr3)): pass + >>> Point2D((1, 2)).x = 2 + Traceback (most recent call last): + ... + ValueError: Ambiguous attribute 'x'. Try one of: normal_x, inverted_x + + >>> Point2D((1, 2)).x + Traceback (most recent call last): + ... + ValueError: Ambiguous attribute 'x'. Try one of: normal_x, inverted_x + + + incorrect attribute + ------------------- + Of course, referring to an attribute label that can't be infered following the above rules then it'll cast an attribute error:: From 52b095436c4a90d85da8e03f4b983973909aeafb Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Mon, 24 Apr 2017 17:06:33 +0800 Subject: [PATCH 09/15] new: HSV support added. !api API breaks is introduced for all code using ``hue``,``saturation`` to access ``HSL`` components of a ``Color`` object, either as attributes or keyword values when instantiating. Please change these to ``hsl_hue``, and ``hsl_saturation``. --- README.rst | 42 ++++++++++++++++++++--------- colour.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 97 insertions(+), 24 deletions(-) diff --git a/README.rst b/README.rst index 7456048..3ffaf58 100644 --- a/README.rst +++ b/README.rst @@ -43,16 +43,16 @@ Feature - Damn simple and pythonic way to manipulate color representation (see examples below) -- Full conversion between RGB, HSL, 6-digit hex, 3-digit hex, human color - -- One object (``Color``) or bunch of single purpose function (``rgb2hex``, - ``hsl2rgb`` ...) +- Full conversion between RGB, HSL, HSV web ready format (see next point) - ``web`` format that use the smallest representation between 6-digit (e.g. ``#fa3b2c``), 3-digit (e.g. ``#fbb``), fully spelled color (e.g. ``white``), following `W3C color naming`_ for compatible CSS or HTML color specifications. +- One object (``Color``) or bunch of single purpose function (``rgb2hex``, + ``hsl2rgb`` ...) + - smooth intuitive color scale generation choosing N color gradients. - can pick colors for you to identify objects of your application. @@ -116,10 +116,10 @@ Please note that all of these are equivalent examples to create the red color:: Color("red") ## human, web compatible representation Color(red=1) ## default amount of blue and green is 0.0 - Color("blue", hue=0) ## hue of blue is 0.66, hue of red is 0.0 + Color("blue", hsl_hue=0) ## hue of blue is 0.66, hue of red is 0.0 Color("#f00") ## standard 3 hex digit web compatible representation Color("#ff0000") ## standard 6 hex digit web compatible representation - Color(hue=0, saturation=1, luminance=0.5) + Color(hsl_hue=0, hsl_saturation=1, hsl_luminance=0.5) Color(hsl=(0, 1, 0.5)) ## full 3-uple HSL specification Color(rgb=(1, 0, 0)) ## full 3-uple RGB specification Color(Color("red")) ## recursion doesn't break object @@ -138,10 +138,12 @@ Several representations are accessible:: 'blue' >>> c.hsl # doctest: +ELLIPSIS HSL(hue=0.66..., saturation=1.0, luminance=0.5) + >>> c.hsv # doctest: +ELLIPSIS + HSV(hue=0.66..., saturation=1.0, value=1.0) >>> c.rgb RGB(red=0.0, green=0.0, blue=1.0) -These two last are ``namedtuple`` and can be used as normal tuples. +These last values are ``namedtuple`` and can be used as normal tuples. And their different sub values are also independently accessible, as the different amount of red, blue, green, in the RGB format:: @@ -161,13 +163,27 @@ case, so:: >>> c.rgb_red == c.red True +So, in the previous example, attributes are resolved as names of component +of the RGB format. In some case, as for ``saturation``, it could +be ambiguous to which format you are referring as more than one format +((HSV and HSL) have a ``saturation`` component that are not valued the +same way. If this happens, you'll get an exception:: + + >>> c.saturation + Traceback (most recent call last): + ... + ValueError: Ambiguous attribute 'saturation'. Try one of: hsl_saturation, hsv_saturation + + >>> c.hsl_saturation + 1.0 + Or the hue, saturation and luminance of the HSL representation:: - >>> c.hue # doctest: +ELLIPSIS + >>> c.hsl_hue # doctest: +ELLIPSIS 0.66... - >>> c.saturation + >>> c.hsl_saturation 1.0 - >>> c.luminance + >>> c.hsl_luminance 0.5 A note on the ``.hex`` property: it'll return the 6 hexadigit, if you @@ -189,7 +205,7 @@ All of these properties are read/write, so let's add some red to this color:: We might want to de-saturate this color:: - >>> c.saturation = 0.5 + >>> c.hsl_saturation = 0.5 >>> c @@ -246,7 +262,7 @@ Color comparison is a vast subject. However, it might seem quite straightforward you. ``Colour`` uses a configurable default way of comparing color that might suit your needs:: - >>> Color("red") == Color("#f00") == Color("blue", hue=0) + >>> Color("red") == Color("#f00") == Color("blue", hsl_hue=0) True The default comparison algorithm focuses only on the "web" representation which is @@ -290,7 +306,7 @@ As you might have already guessed, the sane default is ``RGB_equivalence``, so:: Here's how you could implement your unique comparison function:: - >>> saturation_equivalence = lambda c1, c2: c1.saturation == c2.saturation + >>> saturation_equivalence = lambda c1, c2: c1.hsl_saturation == c2.hsl_saturation >>> red = Color("red", equality=saturation_equivalence) >>> blue = Color("blue", equality=saturation_equivalence) >>> white = Color("white", equality=saturation_equivalence) diff --git a/colour.py b/colour.py index 3023f46..158104e 100644 --- a/colour.py +++ b/colour.py @@ -26,6 +26,7 @@ import sys import traceback import inspect +import colorsys ## @@ -1221,6 +1222,19 @@ class HSL(Tuple("hue", "saturation", "luminance")): """ +@register_format(Formats) +class HSV(Tuple("hue", "saturation", "value")): + """3-uple of Hue, Saturation, Value all between 0.0 and 1.0 + + As all ``Format`` subclass, it can instanciate color based on the X11 + color names:: + + >>> HSV.blue # doctest: +ELLIPSIS + HSV(hue=0.66..., saturation=1.0, value=1.0) + + """ + + @register_format(Formats) class RGB(Tuple("red", "green", "blue")): """3-uple of Red, Green, Blue all values between 0.0 and 1.0 @@ -1243,6 +1257,7 @@ class RGB(Tuple("red", "green", "blue")): """ + @register_format(Formats) class Hex(String): """7-chars string starting with '#' and with red, green, blue values @@ -1739,6 +1754,10 @@ def web2hex(web): return rgb2hex([float(int(v)) / 255 for v in COLOR_NAME_TO_RGB[web]]) +register_converter(Converters, RGB, HSV)(lambda rgb: colorsys.rgb_to_hsv(*rgb)) +register_converter(Converters, HSV, RGB)(lambda hsv: colorsys.hsv_to_rgb(*hsv)) + + class Color(mkDataSpace(formats=Formats, converters=Converters, picker=RGB_color_picker)): """Abstraction of a color object @@ -1755,12 +1774,7 @@ class Color(mkDataSpace(formats=Formats, converters=Converters, Access values ------------- - >>> b.hue # doctest: +ELLIPSIS - 0.66... - >>> b.saturation - 1.0 - >>> b.luminance - 0.5 + Direct attribute will be resolved in available format:: >>> b.red 0.0 @@ -1769,23 +1783,43 @@ class Color(mkDataSpace(formats=Formats, converters=Converters, >>> b.green 0.0 + In the previous example, attributes are resolved as names of + component of the RGB format. In some case you need to be more + specific by prefixing by the format name, so:: + + >>> b.red == b.rgb_red + True + + Hsl components for instances are accessible:: + + >>> b.hsl_saturation + 1.0 + >>> b.hsl_hue # doctest: +ELLIPSIS + 0.66... + >>> b.hsl_luminance + 0.5 + >>> b.rgb RGB(red=0.0, green=0.0, blue=1.0) >>> b.hsl # doctest: +ELLIPSIS HSL(hue=0.66..., saturation=1.0, luminance=0.5) + >>> b.hsv # doctest: +ELLIPSIS + HSV(hue=0.66..., saturation=1.0, value=1.0) >>> b.hex '#0000ff' + >>> b.web + 'blue' Change values ------------- Let's change Hue toward red tint: - >>> b.hue = 0.0 + >>> b.hsl_hue = 0.0 >>> b.hex '#ff0000' - >>> b.hue = 2.0/3 + >>> b.hsl_hue = 2.0/3 >>> b.hex '#0000ff' @@ -1815,11 +1849,11 @@ class Color(mkDataSpace(formats=Formats, converters=Converters, >>> c = Color('blue') >>> c - >>> c.hue = 0 + >>> c.hsl_hue = 0 >>> c - >>> c.saturation = 0.0 + >>> c.hsl_saturation = 0.0 >>> c.hsl # doctest: +ELLIPSIS HSL(hue=..., saturation=0.0, luminance=0.5) >>> c.rgb @@ -1867,7 +1901,30 @@ class Color(mkDataSpace(formats=Formats, converters=Converters, ... AttributeError: 'lightness' not found - TODO: could add HSV, CMYK, YUV conversion. + HSV Support + ----------- + + >>> c = Color('red') + >>> c.hsv + HSV(hue=0.0, saturation=1.0, value=1.0) + >>> c.value + 1.0 + + >>> c.rgb = (0.099, 0.795, 0.591) + >>> c.hsv # doctest: +ELLIPSIS + HSV(hue=0.45..., saturation=0.87..., value=0.79...) + >>> c.hsv = HSV(hue=0.0, saturation=0.5, value=1.0) + >>> c.hex + '#ff7f7f' + + Notice how HSV saturation is different from HSL one: + + >>> c.hsv_saturation # doctest: +ELLIPSIS + 0.5 + >>> c.hsl_saturation # doctest: +ELLIPSIS + 1.0 + + TODO: could add CMYK, YUV conversion. # >>> b.hsv # >>> b.value From 24d44988ca7b4d5e947ddfc0f6eceb154a67766d Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Mon, 24 Apr 2017 21:14:29 +0800 Subject: [PATCH 10/15] new: added implementation for YIQ format. --- README.rst | 4 +++- colour.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 3ffaf58..905b681 100644 --- a/README.rst +++ b/README.rst @@ -43,7 +43,7 @@ Feature - Damn simple and pythonic way to manipulate color representation (see examples below) -- Full conversion between RGB, HSL, HSV web ready format (see next point) +- Full conversion between RGB, HSL, HSV, YIQ web ready format (see next point) - ``web`` format that use the smallest representation between 6-digit (e.g. ``#fa3b2c``), 3-digit (e.g. ``#fbb``), fully spelled @@ -140,6 +140,8 @@ Several representations are accessible:: HSL(hue=0.66..., saturation=1.0, luminance=0.5) >>> c.hsv # doctest: +ELLIPSIS HSV(hue=0.66..., saturation=1.0, value=1.0) + >>> c.yiq # doctest: +ELLIPSIS + YIQ(luma=0.11, inphase=-0.32..., quadrature=0.31...) >>> c.rgb RGB(red=0.0, green=0.0, blue=1.0) diff --git a/colour.py b/colour.py index 158104e..137d724 100644 --- a/colour.py +++ b/colour.py @@ -1226,7 +1226,7 @@ class HSL(Tuple("hue", "saturation", "luminance")): class HSV(Tuple("hue", "saturation", "value")): """3-uple of Hue, Saturation, Value all between 0.0 and 1.0 - As all ``Format`` subclass, it can instanciate color based on the X11 + As all ``Format`` subclass, it can instantiate color based on the X11 color names:: >>> HSV.blue # doctest: +ELLIPSIS @@ -1257,6 +1257,21 @@ class RGB(Tuple("red", "green", "blue")): """ +@register_format(Formats) +class YIQ(Tuple("luma", "inphase", "quadrature")): + """3-uple of luma, inphase, quadrature + + As all ``Format`` subclass, it can instantiate color based on the X11 + color names:: + + >>> YIQ.green # doctest: +ELLIPSIS + YIQ(luma=0.29..., inphase=-0.1..., quadrature=-0.26...) + + Warning, results here will change slightly between python 2.7 and + python 3.4+ + + """ + @register_format(Formats) class Hex(String): @@ -1756,6 +1771,8 @@ def web2hex(web): register_converter(Converters, RGB, HSV)(lambda rgb: colorsys.rgb_to_hsv(*rgb)) register_converter(Converters, HSV, RGB)(lambda hsv: colorsys.hsv_to_rgb(*hsv)) +register_converter(Converters, RGB, YIQ)(lambda rgb: colorsys.rgb_to_yiq(*rgb)) +register_converter(Converters, YIQ, RGB)(lambda yiq: colorsys.yiq_to_rgb(*yiq)) class Color(mkDataSpace(formats=Formats, converters=Converters, @@ -1805,6 +1822,8 @@ class Color(mkDataSpace(formats=Formats, converters=Converters, HSL(hue=0.66..., saturation=1.0, luminance=0.5) >>> b.hsv # doctest: +ELLIPSIS HSV(hue=0.66..., saturation=1.0, value=1.0) + >>> b.yiq # doctest: +ELLIPSIS + YIQ(luma=0.11, inphase=-0.32..., quadrature=0.31...) >>> b.hex '#0000ff' >>> b.web @@ -1924,6 +1943,23 @@ class Color(mkDataSpace(formats=Formats, converters=Converters, >>> c.hsl_saturation # doctest: +ELLIPSIS 1.0 + + YIQ Support + ----------- + + >>> c = Color('green') + >>> c.yiq # doctest: +ELLIPSIS + YIQ(luma=0.29..., inphase=-0.1..., quadrature=-0.26...) + >>> c.yiq_luma # doctest: +ELLIPSIS + 0.29... + + Reversing a YIQ value to RGB:: + + >>> c = Color(c.yiq) + >>> c.hex + '#008000' + + TODO: could add CMYK, YUV conversion. # >>> b.hsv From d3fa5fe8104ffd713f1dbd22fceb444dc8e6fc2c Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Wed, 26 Apr 2017 11:10:27 +0800 Subject: [PATCH 11/15] fix: doc: corrected docstrings of convertors !minor --- colour.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/colour.py b/colour.py index 137d724..c4bdb91 100644 --- a/colour.py +++ b/colour.py @@ -1322,17 +1322,18 @@ class HexS(String): def hsl2rgb(hsl): """Convert HSL representation towards RGB - :param h: Hue, position around the chromatic circle (h=1 equiv h=0) - :param s: Saturation, color saturation (0=full gray, 1=full color) - :param l: Ligthness, Overhaul lightness (0=full black, 1=full white) + :param hsl: 3-uple with + Hue, position around the chromatic circle (h=1 equiv h=0) + Saturation, color saturation (0=full gray, 1=full color) + Ligthness, Overhaul lightness (0=full black, 1=full white) :rtype: 3-uple for RGB values in float between 0 and 1 Hue, Saturation, Range from Lightness is a float between 0 and 1 Note that Hue can be set to any value but as it is a rotation - around the chromatic circle, any value above 1 or below 0 can - be expressed by a value between 0 and 1 (Note that h=0 is equiv - to h=1). + around the chromatic circle, any value above 1 or below 0 can be + expressed by a value between 0 and 1 (Note that h=0 is equiv to + h=1). This algorithm came from: http://www.easyrgb.com/index.php?X=MATH&H=19#text19 @@ -1423,9 +1424,7 @@ def hsl2rgb(hsl): def rgb2hsl(rgb): """Convert RGB representation towards HSL - :param r: Red amount (float between 0 and 1) - :param g: Green amount (float between 0 and 1) - :param b: Blue amount (float between 0 and 1) + :param rgb: 3-uple Red, Green, Blue amount (floats between 0 and 1) :rtype: 3-uple for HSL values in float between 0 and 1 This algorithm came from: @@ -1575,7 +1574,7 @@ def hex2hexs(hex): @register_converter(Converters, HexS, Hex) def hexs2hex(hex): - """Enlarge possible short 3 hexgit to give full hex 6 char long + """Enlarge possible short 3 hex char string to give full hex 6 char string Usage ----- @@ -1603,7 +1602,7 @@ def rgb2hex(rgb): """Transform RGB tuple to hex RGB representation :param rgb: RGB 3-uple of float between 0 and 1 - :rtype: 3 hex char or 6 hex char string representation + :rtype: 3 hex char or 6 hex char string representation with '#' prefix Usage ----- @@ -1631,7 +1630,7 @@ def rgb2hex(rgb): def hex2rgb(str_rgb): """Transform hex RGB representation to RGB tuple - :param str_rgb: 3 hex char or 6 hex char string representation + :param str_rgb: 3 hex char or 6 hex char string representation with '#' prefix. :rtype: RGB 3-uple of float between 0 and 1 >>> from colour import hex2rgb @@ -1673,7 +1672,7 @@ def hex2rgb(str_rgb): def hex2web(hex): """Converts Hex representation to Web - :param rgb: 3 hex char or 6 hex char string representation + :param hex: 3 hex char or 6 hex char string representation :rtype: web string representation (human readable if possible) Web representation uses X11 rgb.txt to define convertion @@ -1709,7 +1708,7 @@ def hex2web(hex): def web2hex(web): """Converts Web representation to Hex - :param rgb: web string representation (human readable if possible) + :param web: web string representation (human readable if possible) :rtype: 3 hex char or 6 hex char string representation Web representation uses X11 rgb.txt (converted in array in this file) From 1f462c2ec0e9a7aaeb17c477a53863f740befbf2 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Wed, 26 Apr 2017 13:43:52 +0800 Subject: [PATCH 12/15] new: implementation of CMY and CMYK support. --- README.rst | 5 ++- colour.py | 127 +++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 123 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 905b681..a13f17e 100644 --- a/README.rst +++ b/README.rst @@ -43,7 +43,8 @@ Feature - Damn simple and pythonic way to manipulate color representation (see examples below) -- Full conversion between RGB, HSL, HSV, YIQ web ready format (see next point) +- Full conversion between RGB, HSL, HSV, YIQ, CMY, CMYK web ready + format (see next point) - ``web`` format that use the smallest representation between 6-digit (e.g. ``#fa3b2c``), 3-digit (e.g. ``#fbb``), fully spelled @@ -142,6 +143,8 @@ Several representations are accessible:: HSV(hue=0.66..., saturation=1.0, value=1.0) >>> c.yiq # doctest: +ELLIPSIS YIQ(luma=0.11, inphase=-0.32..., quadrature=0.31...) + >>> c.cmyk # doctest: +ELLIPSIS + CMYK(cyan=1.0, magenta=1.0, yellow=0.0, key=0.0) >>> c.rgb RGB(red=0.0, green=0.0, blue=1.0) diff --git a/colour.py b/colour.py index c4bdb91..e0bb9a3 100644 --- a/colour.py +++ b/colour.py @@ -1272,6 +1272,34 @@ class YIQ(Tuple("luma", "inphase", "quadrature")): """ +@register_format(Formats) +class CMY(Tuple("cyan", "magenta", "yellow")): + """3-uple of cyan, magenta, and yellow, all values are between 0. and 1. + + As all ``Format`` subclass, it can instanciate color based on the X11 + color names:: + + >>> CMY.CYAN ## Avoid using 'cyan' as it conflict with property + CMY(cyan=1.0, magenta=0.0, yellow=0.0) + + """ + + +@register_format(Formats) +class CMYK(Tuple("cyan", "magenta", "yellow", "key")): + """4-uple of cyan, magenta, yellow, and key all values are between 0 and 1 + + As all ``Format`` subclass, it can instantiate color based on the X11 + color names:: + + >>> CMYK.CYAN ## Avoid using 'cyan' as it conflict with property + CMYK(cyan=1.0, magenta=0.0, yellow=0.0, key=0.0) + >>> CMYK.black + CMYK(cyan=0.0, magenta=0.0, yellow=0.0, key=1.0) + + """ + + @register_format(Formats) class Hex(String): @@ -1772,6 +1800,82 @@ def web2hex(web): register_converter(Converters, HSV, RGB)(lambda hsv: colorsys.hsv_to_rgb(*hsv)) register_converter(Converters, RGB, YIQ)(lambda rgb: colorsys.rgb_to_yiq(*rgb)) register_converter(Converters, YIQ, RGB)(lambda yiq: colorsys.yiq_to_rgb(*yiq)) +register_converter(Converters, RGB, CMY)(lambda rgb: tuple(1 - u for u in rgb)) +register_converter(Converters, CMY, RGB)(lambda rgb: tuple(1 - u for u in rgb)) + + +@register_converter(Converters, CMY, CMYK) +def cmy2cmyk(cmy): + """Converts CMY representation to CMYK + + :param cmy: 3-uple with cyan, magenta, yellow values + :rtype: 4-uple with cyan, magenta, yellow, key values + + Usage + ===== + + >>> from colour import cmy2cmyk + + >>> cmy2cmyk((0, 0, 0)) + (0.0, 0.0, 0.0, 0.0) + + >>> cmy2cmyk((1, 1, 1)) + (0.0, 0.0, 0.0, 1.0) + + >>> cmy2cmyk((0.5, 0.6, 0.7)) # doctest: +ELLIPSIS + (0.0, 0.19..., 0.39..., 0.5) + + >>> cmy2cmyk((2, 0, 0)) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Cyan must be between 0 and 1. You provided 2.0. + + """ + c, m, y = [float(v) for v in cmy] + + for name, v in {'Cyan': c, 'Magenta': m, 'Yellow': y}.items(): + if not (0 - FLOAT_ERROR <= v <= 1 + FLOAT_ERROR): + raise ValueError("%s must be between 0 and 1. You provided %r." + % (name, v)) + k = min(c, m, y) + if k > 1 - FLOAT_ERROR: + return 0., 0., 0., k + + inv = float(1 - k) + + return tuple(((x - k) / inv) for x in cmy) + (k, ) + + +@register_converter(Converters, CMYK, CMY) +def cmyk2cmy(cmyk): + """Converts CMY representation to CMYK + + :param cmyk: 4-uple with cyan, magenta, yellow, key values + :rtype: 3-uple with cyan, magenta, yellow values + + Usage + ===== + + >>> from colour import cmyk2cmy + + >>> cmyk2cmy((0, 0, 0, 0)) + (0.0, 0.0, 0.0) + + >>> cmyk2cmy((0, 0, 0, 1)) + (1.0, 1.0, 1.0) + + >>> cmyk2cmy((2.0, 0, 0, 0)) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Cyan must be between 0 and 1. You provided 2.0. + + """ + c, m, y, k = cmyk + for name, v in {'Cyan': c, 'Magenta': m, 'Yellow': y, 'Key': k}.items(): + if not (0 - FLOAT_ERROR <= v <= 1 + FLOAT_ERROR): + raise ValueError("%s must be between 0 and 1. You provided %r." + % (name, v)) + return tuple((float(x) * (1 - float(k))) + k for x in (c, m, y)) class Color(mkDataSpace(formats=Formats, converters=Converters, @@ -1959,16 +2063,23 @@ class Color(mkDataSpace(formats=Formats, converters=Converters, '#008000' - TODO: could add CMYK, YUV conversion. + CMY/CMYK Support + ----------- + + >>> c = Color('green') + >>> c.cmyk # doctest: +ELLIPSIS + CMYK(cyan=1.0, magenta=0.0, yellow=1.0, key=0.49...) + >>> c.key # doctest: +ELLIPSIS + 0.49... + + Reversing a CMYK value to RGB:: + + >>> c = Color(c.cmyk) + >>> c.hex + '#008000' -# >>> b.hsv -# >>> b.value -# >>> b.cyan -# >>> b.magenta -# >>> b.yellow -# >>> b.key -# >>> b.cmyk + TODO: could add YUV conversion. Recursive init -------------- From a056c46a06d190b7c509e754a90c385c4073ee52 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Tue, 2 May 2017 16:59:43 +0800 Subject: [PATCH 13/15] new: added YUV format conversion. --- README.rst | 2 ++ colour.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index a13f17e..ff62035 100644 --- a/README.rst +++ b/README.rst @@ -147,6 +147,8 @@ Several representations are accessible:: CMYK(cyan=1.0, magenta=1.0, yellow=0.0, key=0.0) >>> c.rgb RGB(red=0.0, green=0.0, blue=1.0) + >>> c.yuv + YUV(luma=0.114, u=0.436, v=-0.10001) These last values are ``namedtuple`` and can be used as normal tuples. diff --git a/colour.py b/colour.py index e0bb9a3..12e548b 100644 --- a/colour.py +++ b/colour.py @@ -492,6 +492,29 @@ def _f(value): % (src_format, dst_format)) +class Matrix(object): + """Simple matrix calculus + + >>> Matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]])([1, 2, 3]) + (1, 2, 3) + + >>> Matrix([[1, 0, 0], [0, 2, 0], [0, 0, 3]]) * [1, 1, 1] + (1, 2, 3) + + """ + + def __init__(self, m): + self._m = m + + def __call__(self, v): + return tuple( + sum(x1 * x2 for x1, x2 in zip(u, v)) + for u in self._m) + + def __mul__(self, v): + return self.__call__(v) + + def register_converter(registry, src, dst, **kwargs): def decorator(f): @@ -1299,6 +1322,18 @@ class CMYK(Tuple("cyan", "magenta", "yellow", "key")): """ +@register_format(Formats) +class YUV(Tuple("luma", "u", "v")): + """3-uple of Luma, U, V. + + Luma is between 0.0 and 1.0, U and V are coordinate + with values between -1.0 and 1.0. + + >>> YUV.blue + YUV(luma=0.114, u=0.436, v=-0.10001) + + """ + @register_format(Formats) @@ -1878,6 +1913,54 @@ def cmyk2cmy(cmyk): return tuple((float(x) * (1 - float(k))) + k for x in (c, m, y)) +RGB_TO_YUV = Matrix([ + [0.299, 0.587, 0.114], + [-0.14713, -0.28886, 0.436], + [0.615, -0.51499, -0.10001], +]) + + +@register_converter(Converters, RGB, YUV) +def rgb2yuv(rgb): + """Converting from RGB to YUV using BT.709 conversion + + cf. https://en.wikipedia.org/wiki/YUV + + >>> rgb2yuv((1., 0., 0.)) + (0.299, -0.14713, 0.615) + + """ + return RGB_TO_YUV(rgb) + + +## Use: +## +## >>> import colour +## >>> from numpy import matrix, linalg +## >>> print (linalg.inv(RGB_TO_YUV)) +## +## To get the reverse matrix. +YUV_TO_RGB = Matrix([ + [1., -0.0000117983844, 1.13983458], + [1.00000395, -0.394646053, -0.580594234], + [0.999979679, 2.03211194, -0.0000151129807], +]) + + +@register_converter(Converters, YUV, RGB) +def yuv2rgb(yuv): + """Converting from YUV to RGB using BT.709 conversion + + cf. https://en.wikipedia.org/wiki/YUV + + >>> yuv2rgb((1., 0., 0.)) # doctest: +ELLIPSIS + (1.0, 1.0..., 0.99...) + + """ + + return YUV_TO_RGB(yuv) + + class Color(mkDataSpace(formats=Formats, converters=Converters, picker=RGB_color_picker)): """Abstraction of a color object @@ -1931,6 +2014,8 @@ class Color(mkDataSpace(formats=Formats, converters=Converters, '#0000ff' >>> b.web 'blue' + >>> b.yuv # doctest: +ELLIPSIS + YUV(luma=0.114, u=0.436, v=-0.10001) Change values ------------- @@ -2046,7 +2131,6 @@ class Color(mkDataSpace(formats=Formats, converters=Converters, >>> c.hsl_saturation # doctest: +ELLIPSIS 1.0 - YIQ Support ----------- @@ -2079,8 +2163,6 @@ class Color(mkDataSpace(formats=Formats, converters=Converters, '#008000' - TODO: could add YUV conversion. - Recursive init -------------- From ee17a6b99a5a159ddaab64d644f75d2d1b4f9cc0 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Fri, 5 May 2017 11:33:30 +0800 Subject: [PATCH 14/15] chg: dev: pythonic refactor of ``rgb2hsl`` code. !minor --- colour.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/colour.py b/colour.py index 12e548b..7ecb07c 100644 --- a/colour.py +++ b/colour.py @@ -1573,20 +1573,16 @@ def rgb2hsl(rgb): s = diff / vsum else: s = diff / (2.0 - vsum) + dr, dg, db = tuple((vmax - x) / diff for x in (r, g, b)) - dr = (((vmax - r) / 6) + (diff / 2)) / diff - dg = (((vmax - g) / 6) + (diff / 2)) / diff - db = (((vmax - b) / 6) + (diff / 2)) / diff + h = db - dg if r == vmax else \ + 2. + dr - db if g == vmax else \ + 4. + dg - dr - if r == vmax: - h = db - dg - elif g == vmax: - h = (1.0 / 3) + dr - db - else: ## b == vmax - h = (2.0 / 3) + dg - dr + h /= 6 - if h < 0: h += 1 - if h > 1: h -= 1 + while h < 0: h += 1 + while h > 1: h -= 1 return h, s, l From 870dc944da722bd8a898825233c8843783d96b3a Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Fri, 5 May 2017 17:02:31 +0800 Subject: [PATCH 15/15] new: any ``ConvertorRegistry`` provide now all conversion methods as ``.xxx2yyy(..)``. In particular, ``colour.Convertors`` provides all possible conversion methods implemented and integrated in current ``colour`` version. --- colour.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/colour.py b/colour.py index 7ecb07c..a6bd433 100644 --- a/colour.py +++ b/colour.py @@ -420,30 +420,58 @@ class ConverterRegistry(list): >>> cr.convert_fun("hex", "dec")("15") 21 + The most convenient way to access convertors is to use the + 'xxx2yyy' attributes of a ``ConverterRegistry``, so the last + instruction is equivalent to:: + + >>> cr.hex2dec("15") + 21 + Note that this is provided directly by only one converter, in the following 2 converters will be used to get to the answer:: - >>> cr.convert_fun("hex", "bin")("15") + >>> cr.hex2bin("15") '0b10101' When source and destination format are equivalent, this will make not change on the output:: - >>> cr.convert_fun("hex", "hex")("15") + >>> cr.hex2hex("15") '15' And if no path exists it'll cast an exception:: - >>> cr.convert_fun("bin", "hex")("0101") + >>> cr.bin2hex("0101") Traceback (most recent call last): ... ValueError: No convertion path found from bin to hex format. + If one of the 2 part of 'xxx2yyy' is not a valid format label, + it will complain:: + + >>> cr.foo2hex("0101") + Traceback (most recent call last): + ... + AttributeError: Unknown format labeled foo. + + >>> cr.hex2foo("0101") + Traceback (most recent call last): + ... + AttributeError: Unknown format labeled foo. + + + If not a 'xxx2yyy' format, it'll cast normal attribute error:: + + >>> cr.foo("0101") + Traceback (most recent call last): + ... + AttributeError: no attribute 'foo' + Note that if the functions have already been annotated, then you can instantiate directly a new ``ConverterRegistry``:: >>> new_cr = ConverterRegistry(cr) - >>> new_cr.convert_fun("hex", "bin")("15") + >>> new_cr.hex2bin("15") '0b10101' """ @@ -453,6 +481,14 @@ def __init__(self, converters=None): converters = [] super(ConverterRegistry, self).__init__(converters) + @property + def formats(self): + def i(): + for cv in self: + yield cv.src + yield cv.dst + return set(i()) + def get(self, src): return {cv.dst: (cv, cv.conv_kwargs) for cv in self @@ -487,10 +523,29 @@ def _f(value): path = self.find_path(src_format, dst_format) if path: return _path_to_callable(path) + path = self.find_path(src_format, dst_format) raise ValueError( "No convertion path found from %s to %s format." % (src_format, dst_format)) + def __getattr__(self, label): + m = re.match("(?P[a-zA-Z0-9_]+)2(?P[a-zA-Z0-9_]+)", label) + if m is None: + raise AttributeError( + 'no attribute %r' + % (label, )) + dct = m.groupdict() + for target, label in list(dct.items()): + for f in self.formats: + if str(f) == label: + dct["c%s" % target] = f + break + else: + raise AttributeError( + "Unknown format labeled %s." + % (label, )) + return self.convert_fun(dct["csrc"], dct["cdst"]) + class Matrix(object): """Simple matrix calculus