diff --git a/.travis.yml b/.travis.yml index fe9f9a5..dc558a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,10 +9,15 @@ 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" - + - 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/README.rst b/README.rst index 7e1d4c9..ff62035 100644 --- a/README.rst +++ b/README.rst @@ -37,27 +37,39 @@ 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) -- 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, 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 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. - .. _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 ============ @@ -105,10 +117,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 @@ -120,14 +132,28 @@ Reading values Several representations are accessible:: >>> c.hex + '#0000ff' + >>> c.hexs '#00f' + >>> c.web + 'blue' >>> c.hsl # doctest: +ELLIPSIS - (0.66..., 1.0, 0.5) + 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.cmyk # doctest: +ELLIPSIS + CMYK(cyan=1.0, magenta=1.0, yellow=0.0, key=0.0) >>> c.rgb - (0.0, 0.0, 1.0) + RGB(red=0.0, green=0.0, blue=1.0) + >>> c.yuv + YUV(luma=0.114, u=0.436, v=-0.10001) -And their different parts are also independently accessible, as the different -amount of red, blue, green, in the RGB format:: +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:: >>> c.red 0.0 @@ -136,21 +162,43 @@ 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 + +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 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 @@ -164,7 +212,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 @@ -221,7 +269,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 @@ -265,7 +313,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) @@ -343,6 +391,7 @@ And inequality (using ``__ne__``) are also polite:: Picking arbitrary color for a python object ------------------------------------------- + Basic Usage ~~~~~~~~~~~ @@ -363,6 +412,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 +440,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 +499,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/appveyor.yml b/appveyor.yml index a2a03cc..e6b7c51 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -37,7 +37,17 @@ 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" + - | + IF DEFINED DOVIS IF DEFINED ARTIFACT_DIR ( + cp .coverage %ARTIFACT_DIR% + echo %cd% > %ARTIFACT_DIR%/cover_path + ) \ No newline at end of file diff --git a/colour.py b/colour.py index fd21013..a6bd433 100644 --- a/colour.py +++ b/colour.py @@ -6,41 +6,42 @@ 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 +import collections import hashlib import re import sys +import traceback +import inspect +import colorsys +## +## 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 ## @@ -199,148 +200,1318 @@ 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) -class C_HSL: + def get_by_attr(self, label): + 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: + 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): + 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 rgb2hsl(tuple(v / 255. for v in COLOR_NAME_TO_RGB[label])) + 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 + ----- -HSL = C_HSL() + >>> Tuple("a", "b", "c") + + This can conveniently be used as a parent class for formats, with + an easy declaration:: -class C_RGB: - """RGB colors container + >>> class MyFormat(Tuple("a", "b", "c")): pass - Provides a quick color access. + .. sensible representation:: - >>> from colour import RGB + >>> MyFormat(1, 2, 3) + MyFormat(a=1, b=2, c=3) - >>> RGB.WHITE - (1.0, 1.0, 1.0) - >>> RGB.BLUE - (0.0, 0.0, 1.0) + .. and the ability to take a real tuple upon initialisation:: - >>> RGB.DONOTEXISTS # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - AttributeError: ... has no attribute 'DONOTEXISTS' + >>> 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 """ - def __getattr__(self, value): - return hsl2rgb(getattr(HSL, value)) + 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), ) -class C_HEX: - """RGB colors container + 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. - Provides a quick color access. + >>> red = MyFormat('red') - >>> from colour import HEX + Notice that the representation of this object is invisible as it + is a subclass of string:: - >>> HEX.WHITE - '#fff' - >>> HEX.BLUE - '#00f' + >>> red + 'red' - >>> HEX.DONOTEXISTS # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - AttributeError: ... has no attribute 'DONOTEXISTS' + Although:: + + >>> type(MyFormat('red')) + + + You can avoid setting ``regex`` if you have no use of this check:: + + >>> class MyFormat(String): pass + >>> MyFormat('red') + 'red' """ - def __getattr__(self, value): - return rgb2hex(getattr(RGB, value)) + 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 + + 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.hex2bin("15") + '0b10101' + + When source and destination format are equivalent, this will make not change + on the output:: + + >>> cr.hex2hex("15") + '15' + + And if no path exists it'll cast an exception:: + + >>> 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.hex2bin("15") + '0b10101' + + """ + + def __init__(self, converters=None): + if converters is 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 + 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) + 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 + + >>> 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): + 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. + + 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:: + + >>> 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 +## + +RGB_equivalence = lambda c1, c2: c1.hex == c2.hex +HSL_equivalence = lambda c1, c2: c1.hsl == c2.hsl + + +## +## Module wide color object +## + + +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 + + +## +## Convenience +## + +def format_last_exception(prefix=" | "): + """Format the last exception for display it in tests. + + 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 + + """ + + return '\n'.join( + str(prefix + line) + for line in traceback.format_exc().strip().split('\n')) + + +## +## Color Formats +## + +## 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 + + This format is used most notably in HTML/CSS for its ease of use. + + 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' + + """ + + default = 'blue' + + @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 + + +@register_format(Formats) +class HSL(Tuple("hue", "saturation", "luminance")): + """3-uple of Hue, Saturation, Lightness all between 0.0 and 1.0 + + 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) + + """ + + +@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 instantiate 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 + + As all ``Format`` subclass, it can instantiate color based on the X11 + color names:: + + >>> 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 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 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 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) +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})?$') + + +## +## Converters +## -RGB = C_RGB() -HEX = C_HEX() +## Module wide converters +Converters = ConverterRegistry() -## -## Convertion function -## +@register_converter(Converters, HSL, RGB) 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 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] @@ -367,12 +1538,11 @@ def hsl2rgb(hsl): return r, g, b +@register_converter(Converters, RGB, 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: @@ -380,54 +1550,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)) - (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] @@ -446,7 +1617,7 @@ def rgb2hsl(rgb): l = vsum / 2 if diff < FLOAT_ERROR: ## This is a gray, no chroma... - return (0.0, 0.0, l) + return 0.0, 0.0, l ## ## Chromatic data... @@ -457,22 +1628,18 @@ 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 - elif 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) + return h, s, l def _hue2rgb(v1, v2, vH): @@ -492,67 +1659,111 @@ def _hue2rgb(v1, v2, vH): return v1 -def rgb2hex(rgb, force_long=False): - """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 +@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 - >>> rgb2hex((0.0,1.0,0.0)) - '#0f0' + Provided a long string hex format, it should shorten it when + possible:: - Rounding try to be as natural as possible: + >>> hex2hexs('#00ff00') + '#0f0' + + In the following case, it is not possible to shorten, thus:: + + >>> hex2hexs('#01ff00') + '#01ff00' + + """ + + if len(hex) == 7 and hex[1::2] == hex[2::2]: + return "#" + ''.join(hex[1::2]) + return hex + + +@register_converter(Converters, HexS, Hex) +def hexs2hex(hex): + """Enlarge possible short 3 hex char string to give full hex 6 char string - >>> rgb2hex((0.0,0.999999,1.0)) - '#0ff' + Usage + ----- + + >>> from colour import hexs2hex + + Provided a short string hex format, it should enlarge it:: - And if not possible, the 6 hex char representation is used: + >>> hexs2hex('#0f0') + '#00ff00' - >>> rgb2hex((0.23,1.0,1.0)) - '#3bffff' + In the following case, it is already enlargened, thus:: - >>> rgb2hex((0.0,0.999999,1.0), force_long=True) - '#00ffff' + >>> 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 + :rtype: 3 hex char or 6 hex char string representation with '#' prefix + + Usage + ----- + + >>> from colour import rgb2hex + + >>> rgb2hex((0.0, 1.0, 0.0)) + '#00ff00' + + Rounding try to be as natural as possible: + + >>> rgb2hex((0.0, 0.999999, 1.0)) + '#00ffff' - hx = ''.join(["%02x" % int(c * 255 + 0.5 - FLOAT_ERROR) - for c in rgb]) + >>> rgb2hex((0.5, 0.999999, 1.0)) + '#7fffff' - if not force_long and hx[0::2] == hx[1::2]: - hx = ''.join(hx[0::2]) + """ - return "#%s" % 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 - :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 + >>> 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 for rgb color. + >>> 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:] @@ -564,37 +1775,36 @@ 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 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 + :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 + Web representation uses X11 rgb.txt to define convertion between RGB and english color names. Usage ===== - >>> from colour import hex2web - - >>> hex2web('#ff0000') - 'red' + >>> from colour import hex2web - >>> hex2web('#aaaaaa') - '#aaa' + >>> hex2web('#ff0000') + 'red' - >>> hex2web('#abc') - '#abc' + >>> hex2web('#aaaaaa') + '#aaa' - >>> hex2web('#acacac') - '#acacac' + >>> hex2web('#acacac') + '#acacac' """ dec_rgb = tuple(int(v * 255) for v in hex2rgb(hex)) @@ -605,68 +1815,63 @@ 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 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) + :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 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 ===== - >>> from colour import web2hex + >>> from colour import web2hex - >>> web2hex('red') - '#f00' - - >>> web2hex('#aaa') - '#aaa' + >>> web2hex('red') + '#ff0000' - >>> web2hex('#foo') # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - AttributeError: '#foo' is not in web format. Need 3 or 6 hex digit. + >>> web2hex('#aaa') + '#aaaaaa' - >>> web2hex('#aaa', force_long=True) - '#aaaaaa' + >>> 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('#'): - 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) @@ -674,131 +1879,146 @@ 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]]) - return rgb2hex([float(int(v)) / 255 for v in COLOR_NAME_TO_RGB[web]], - force_long) +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)) +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)) -## 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)) +@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 -def color_scale(begin_hsl, end_hsl, nb): - """Returns a list of nb color HSL tuples between begin_hsl and end_hsl + Usage + ===== - >>> from colour import color_scale + >>> from colour import cmy2cmyk - >>> [rgb2hex(hsl2rgb(hsl)) for hsl in color_scale((0, 1, 0.5), - ... (1, 1, 0.5), 3)] - ['#f00', '#0f0', '#00f', '#f00'] + >>> cmy2cmyk((0, 0, 0)) + (0.0, 0.0, 0.0, 0.0) - >>> [rgb2hex(hsl2rgb(hsl)) - ... for hsl in color_scale((0, 0, 0), - ... (0, 0, 1), - ... 15)] # doctest: +ELLIPSIS - ['#000', '#111', '#222', ..., '#ccc', '#ddd', '#eee', '#fff'] + >>> cmy2cmyk((1, 1, 1)) + (0.0, 0.0, 0.0, 1.0) - Of course, asking for negative values is not supported: + >>> cmy2cmyk((0.5, 0.6, 0.7)) # doctest: +ELLIPSIS + (0.0, 0.19..., 0.39..., 0.5) - >>> color_scale((0, 1, 0.5), (1, 1, 0.5), -2) - Traceback (most recent call last): - ... - ValueError: Unsupported negative number of colors (nb=-2). + >>> 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] - if nb < 0: - raise ValueError( - "Unsupported negative number of colors (nb=%r)." % nb) + 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 - step = tuple([float(end_hsl[i] - begin_hsl[i]) / nb for i in range(0, 3)]) \ - if nb > 0 else (0, 0, 0) + inv = float(1 - k) - def mul(step, value): - return tuple([v * value for v in step]) + return tuple(((x - k) / inv) for x in cmy) + (k, ) - 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)] +@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 -## -## Color Pickers -## + Usage + ===== -def RGB_color_picker(obj): - """Build a color representation from the string representation of an object + >>> from colour import cmyk2cmy - 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:: + >>> cmyk2cmy((0, 0, 0, 0)) + (0.0, 0.0, 0.0) - >>> from colour import RGB_color_picker, Color + >>> cmyk2cmy((0, 0, 0, 1)) + (1.0, 1.0, 1.0) - Same inputs produce the same result:: + >>> 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. - >>> RGB_color_picker("Something") == RGB_color_picker("Something") - True + """ + 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)) - ... but different inputs produce different colors:: - >>> RGB_color_picker("Something") != RGB_color_picker("Something else") - True +RGB_TO_YUV = Matrix([ + [0.299, 0.587, 0.114], + [-0.14713, -0.28886, 0.436], + [0.615, -0.51499, -0.10001], +]) - In any case, we still get a ``Color`` object:: - >>> isinstance(RGB_color_picker("Something"), Color) - True +@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) - ## 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)] +## 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], +]) - ## 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! +@register_converter(Converters, YUV, RGB) +def yuv2rgb(yuv): + """Converting from YUV to RGB using BT.709 conversion + cf. https://en.wikipedia.org/wiki/YUV -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 type(obj).__name__ + str(obj) + >>> yuv2rgb((1., 0., 0.)) # doctest: +ELLIPSIS + (1.0, 1.0..., 0.99...) + """ + + return YUV_TO_RGB(yuv) -## -## 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 @@ -808,12 +2028,7 @@ class Color(object): 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 @@ -822,44 +2037,68 @@ class Color(object): >>> 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 - (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.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 - '#00f' + '#0000ff' + >>> b.web + 'blue' + >>> b.yuv # doctest: +ELLIPSIS + YUV(luma=0.114, u=0.436, v=-0.10001) Change values ------------- Let's change Hue toward red tint: - >>> b.hue = 0.0 + >>> b.hsl_hue = 0.0 >>> b.hex - '#f00' + '#ff0000' - >>> b.hue = 2.0/3 + >>> b.hsl_hue = 2.0/3 >>> b.hex - '#00f' + '#0000ff' In the other way round: - >>> b.hex = '#f00' + >>> b.hexs = '#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: - >>> 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 @@ -868,15 +2107,15 @@ class Color(object): >>> 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 - (..., 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 @@ -887,15 +2126,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 @@ -914,15 +2159,59 @@ class Color(object): ... 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 + + 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:: -# >>> b.hsv -# >>> b.value -# >>> b.cyan -# >>> b.magenta -# >>> b.yellow -# >>> b.key -# >>> b.cmyk + >>> c = Color(c.yiq) + >>> c.hex + '#008000' + + + 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' Recursive init @@ -966,159 +2255,10 @@ 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) """ - _hsl = None ## internal representation - - 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_"): - 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 not in ["_hsl", "equality"]: - fc = getattr(self, 'set_' + label) - fc(value) - else: - self.__dict__[label] = value - - ## - ## Get - ## - - def get_hsl(self): - return tuple(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 - ## - - 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) - - ## 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 - ## - - 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 + for hsl in color_scale(self.hsl, self.__class__(value).hsl, steps - 1): + yield self.__class__(hsl=hsl) 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