pax_global_header00006660000000000000000000000064140237701720014515gustar00rootroot0000000000000052 comment=6ac9b9d32eeea6f73c336bea9a8c562097767960 colorzero-release-2.0/000077500000000000000000000000001402377017200150125ustar00rootroot00000000000000colorzero-release-2.0/.github/000077500000000000000000000000001402377017200163525ustar00rootroot00000000000000colorzero-release-2.0/.github/workflows/000077500000000000000000000000001402377017200204075ustar00rootroot00000000000000colorzero-release-2.0/.github/workflows/test.yml000066400000000000000000000014701402377017200221130ustar00rootroot00000000000000# Install Python deps, run tests thru pytest and report combined coverage name: colorzero-test-suite on: push: branches: [master] pull_request: branches: [master] jobs: test: runs-on: ubuntu-latest timeout-minutes: 10 strategy: matrix: python-version: - "3.5" - "3.6" - "3.7" - "3.8" - "3.9" - pypy-3.6 - pypy-3.7 steps: - name: Checkout uses: actions/checkout@v2 - name: Install python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | sudo apt install exuberant-ctags make develop - name: Run tests run: make test colorzero-release-2.0/.gitignore000066400000000000000000000003061402377017200170010ustar00rootroot00000000000000*.py[cdo] *.vim *.swp tags # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg # Unit test / coverage reports coverage .coverage* .tox .cache .pytest_cache colorzero-release-2.0/LICENSE.txt000066400000000000000000000027401402377017200166400ustar00rootroot00000000000000SPDX-License-Identifier: BSD-3-Clause Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. colorzero-release-2.0/MANIFEST.in000066400000000000000000000000621402377017200165460ustar00rootroot00000000000000prune docs include README.rst include LICENSE.txt colorzero-release-2.0/Makefile000066400000000000000000000057011402377017200164550ustar00rootroot00000000000000# vim: set noet sw=4 ts=4 fileencoding=utf-8: # External utilities PYTHON=python3 PIP=pip PYTEST=pytest TWINE=twine PYFLAGS= DEST_DIR=/ # Calculate the base names of the distribution, the location of all source, # documentation, packaging, icon, and executable script files NAME:=$(shell $(PYTHON) $(PYFLAGS) setup.py --name) PKG_DIR:=$(subst -,_,$(NAME)) VER:=$(shell $(PYTHON) $(PYFLAGS) setup.py --version) PY_SOURCES:=$(shell \ $(PYTHON) $(PYFLAGS) setup.py egg_info >/dev/null 2>&1 && \ grep -v "\.egg-info" $(PKG_DIR).egg-info/SOURCES.txt) DOC_SOURCES:=docs/conf.py \ $(wildcard docs/*.png) \ $(wildcard docs/*.svg) \ $(wildcard docs/*.dot) \ $(wildcard docs/*.mscgen) \ $(wildcard docs/*.gpi) \ $(wildcard docs/*.rst) \ $(wildcard docs/*.pdf) SUBDIRS:= # Calculate the name of all outputs DIST_WHEEL=dist/$(NAME)-$(VER)-py2.py3-none-any.whl DIST_TAR=dist/$(NAME)-$(VER).tar.gz DIST_ZIP=dist/$(NAME)-$(VER).zip # Default target all: @echo "make install - Install on local system" @echo "make develop - Install symlinks for development" @echo "make test - Run tests" @echo "make doc - Generate HTML and PDF documentation" @echo "make source - Create source package" @echo "make wheel - Generate a PyPI wheel package" @echo "make zip - Generate a source zip package" @echo "make tar - Generate a source tar package" @echo "make dist - Generate all packages" @echo "make clean - Get rid of all generated files" @echo "make release - Create and tag a new release" @echo "make upload - Upload the new release to repositories" install: $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py install --root $(DEST_DIR) doc: $(DOC_SOURCES) $(MAKE) -C docs clean $(MAKE) -C docs html $(MAKE) -C docs epub $(MAKE) -C docs latexpdf source: $(DIST_TAR) $(DIST_ZIP) wheel: $(DIST_WHEEL) zip: $(DIST_ZIP) tar: $(DIST_TAR) dist: $(DIST_WHEEL) $(DIST_TAR) $(DIST_ZIP) develop: tags @# These have to be done separately to avoid a cockup... $(PIP) install -U setuptools $(PIP) install -U pip $(PIP) install -e .[doc,test] test: $(PYTEST) tests clean: rm -fr dist/ build/ .pytest_cache/ $(NAME).egg-info/ tags .coverage for dir in $(SUBDIRS); do \ $(MAKE) -C $$dir clean; \ done find $(CURDIR) -name "*.pyc" -delete tags: $(PY_SOURCES) ctags -R --exclude="build/*" --exclude="docs/*" --languages="Python" $(SUBDIRS): $(MAKE) -C $@ $(DIST_TAR): $(PY_SOURCES) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py sdist --formats gztar $(DIST_ZIP): $(PY_SOURCES) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py sdist --formats zip $(DIST_WHEEL): $(PY_SOURCES) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py bdist_wheel --universal release: $(MAKE) clean test -z "$(shell git status --porcelain)" git tag -s release-$(VER) -m "Release $(VER)" git push origin release-$(VER) $(MAKE) $(DIST_TAR) $(DIST_WHEEL) $(TWINE) check $(DIST_TAR) $(DIST_WHEEL) $(TWINE) upload $(DIST_TAR) $(DIST_WHEEL) .PHONY: all install develop test doc source egg wheel zip tar dist clean tags release upload $(SUBDIRS) colorzero-release-2.0/README.rst000066400000000000000000000054321402377017200165050ustar00rootroot00000000000000.. -*- rst -*- ========= colorzero ========= colorzero is a color manipulation library for Python (yes, *another* one) which aims to be reasonably simple to use and "pythonic" in nature. It does *not* aim to be as comprehensive, powerful, or that matter as *correct* as, say, `colormath`_. colorzero originally grew out of work on my `picamera`_ project, hence it's intended to be sufficiently simple that school children can use it without having to explain color spaces and illuminants. However, it does aim to be useful to a wide range of skills, hence it does include basic facilities for `CIE Lab`_ representations, and `Delta-E`_ calculations should you need them. The major difference between colorzero and other libraries (`grapefruit`_, `colormath`_, etc.) is that its ``Color`` class is a ``namedtuple`` descendent. This means it is immutable; you cannot *directly* change the attributes of a ``Color`` instance. The major advantage of this is that instances can be used as keys in dictionaries (for simple `LUTs`_), or placed in sets. Manipulation of ``Color`` instances is done by typical operations with other classes the result of which is a new ``Color`` instance. For example:: >>> Color('red') + Color('blue') >>> Color('magenta') - Color('red') >>> Color('red') - Red(0.5) >>> Color('green') + Color('grey').red >>> Color.from_hls(0.5, 0.5, 1.0) >>> Color.from_hls(0.5, 0.5, 1.0) * Lightness(0.8) >>> (Color.from_hls(0.5, 0.5, 1.0) * Lightness(0.8)).hls HLS(h=0.5, l=0.4, s=1.0) Links ===== * The code is licensed under the `BSD license`_ * The `source code`_ can be obtained from GitHub, which also hosts the `bug tracker`_ * The `documentation`_ (which includes installation, quick-start examples, and lots of code recipes) can be read on ReadTheDocs * Packages can be downloaded from `PyPI`_, but reading the installation instructions is more likely to be useful .. _picamera: https://picamera.readthedocs.io/ .. _colormath: https://python-colormath.readthedocs.io/ .. _grapefruit: https://grapefruit.readthedocs.io/ .. _CIE Lab: https://en.wikipedia.org/wiki/Lab_color_space .. _Delta-E: https://en.wikipedia.org/wiki/Color_difference .. _PyPI: http://pypi.python.org/pypi/colorzero/ .. _documentation: http://colorzero.readthedocs.io/ .. _source code: https://github.com/waveform80/colorzero .. _bug tracker: https://github.com/waveform80/colorzero/issues .. _BSD license: http://opensource.org/licenses/BSD-3-Clause .. _LUTs: https://en.wikipedia.org/wiki/Lookup_table#Lookup_tables_in_image_processing colorzero-release-2.0/colorzero/000077500000000000000000000000001402377017200170305ustar00rootroot00000000000000colorzero-release-2.0/colorzero/__init__.py000066400000000000000000000015561402377017200211500ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2016-2018 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause """ The colorzero package defines a number of classes for representation and manipulation of colors. The primary class of interest to users is :class:`Color`. The other classes are used for manipulation of the attributes on this class and are: * :class:`Red` * :class:`Green` * :class:`Blue` * :class:`Hue` * :class:`Lightness` * :class:`Saturation` * :class:`Luma` """ from .color import Color from .easings import linear, ease_in, ease_out, ease_in_out from .deltae import euclid, cie1976, cie1994g, cie1994t, ciede2000 from .attr import Red, Green, Blue, Hue, Lightness, Saturation, Luma from .types import RGB, HLS, HSV, CMY, CMYK, YUV, YIQ, XYZ, Luv, Lab from .tables import NAMED_COLORS colorzero-release-2.0/colorzero/attr.py000066400000000000000000000147751402377017200203720ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2016-2021 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause """ Defines the classes for manipulating the attributes of the :class:`Color` class through the standard binary operators. """ from math import pi class Red(float): """ Represents the red component of a :class:`Color` for use in transformations. Instances of this class can be constructed directly with a float value, or by querying the :attr:`Color.red` attribute. Addition, subtraction, and multiplication are supported with :class:`Color` instances. For example:: >>> Color.from_rgb(0, 0, 0) + Red(0.5) >>> Color('#f00') - Color('#900').red >>> (Red(0.1) * Color('red')).red Red(0.1) """ def __repr__(self): return "Red({:g})".format(self) class Green(float): """ Represents the green component of a :class:`Color` for use in transformations. Instances of this class can be constructed directly with a float value, or by querying the :attr:`Color.green` attribute. Addition, subtraction, and multiplication are supported with :class:`Color` instances. For example:: >>> Color(0, 0, 0) + Green(0.1) >>> Color.from_yuv(1, -0.4, -0.6) - Green(1) >>> (Green(0.5) * Color('white')).rgb RGB(r=1.0, g=0.5, b=1.0) """ def __repr__(self): return "Green({:g})".format(self) class Blue(float): """ Represents the blue component of a :class:`Color` for use in transformations. Instances of this class can be constructed directly with a float value, or by querying the :attr:`Color.blue` attribute. Addition, subtraction, and multiplication are supported with :class:`Color` instances. For example:: >>> Color(0, 0, 0) + Blue(0.2) >>> Color.from_hls(0.5, 0.5, 1.0) - Blue(1) >>> Blue(0.9) * Color('white') """ def __repr__(self): return "Blue({:g})".format(self) class Hue(float): """ Represents the hue of a :class:`Color` for use in transformations. Instances of this class can be constructed directly with a float value in the range [0.0, 1.0) representing an angle around the `HSL hue wheel`_. As this is a circular mapping, 0.0 and 1.0 effectively mean the same thing, i.e. out of range values will be normalized into the range [0.0, 1.0). The class can also be constructed with the keyword arguments ``deg`` or ``rad`` if you wish to specify the hue value in degrees or radians instead, respectively. Instances can also be constructed by querying the :attr:`Color.hue` attribute. Addition, subtraction, and multiplication are supported with :class:`Color` instances. For example:: >>> Color(1, 0, 0).hls HLS(h=0.0, l=0.5, s=1.0) >>> (Color(1, 0, 0) + Hue(deg=180)).hls HLS(h=0.5, l=0.5, s=1.0) Note that whilst multiplication by a :class:`Hue` doesn't make much sense, it is still supported. However, the circular nature of a hue value can lead to suprising effects. In particular, since 1.0 is equivalent to 0.0 the following may be observed:: >>> (Hue(1.0) * Color.from_hls(0.5, 0.5, 1.0)).hls HLS(h=0.0, l=0.5, s=1.0) .. _HSL hue wheel: https://en.wikipedia.org/wiki/Hue """ def __new__(cls, n=None, deg=None, rad=None): if n is not None: return super().__new__(cls, n % 1.0) elif deg is not None: return super().__new__(cls, (deg / 360.0) % 1.0) elif rad is not None: return super().__new__(cls, (rad / (2 * pi)) % 1.0) else: raise ValueError('You must specify a value, or deg or rad') def __repr__(self): return "Hue(deg={self.deg:g})".format(self=self) @property def deg(self): """ Returns the hue as a value in degrees with the range 0.0 <= n < 360.0. """ return self * 360.0 @property def rad(self): """ Returns the hue as a value in radians with the range 0.0 <= n < 2π. """ return self * 2 * pi class Lightness(float): """ Represents the lightness of a :class:`Color` for use in transformations. Instances of this class can be constructed directly with a float value, or by querying the :attr:`Color.lightness` attribute. Addition, subtraction, and multiplication are supported with :class:`Color` instances. For example:: >>> Color(0, 0, 0) + Lightness(0.1) >>> Color.from_rgb_bytes(0x80, 0x80, 0) - Lightness(0.2) >>> Lightness(0.9) * Color('wheat') """ def __repr__(self): return "Lightness({:g})".format(self) class Saturation(float): """ Represents the saturation of a :class:`Color` for use in transformations. Instances of this class can be constructed directly with a float value, or by querying the :attr:`Color.saturation` attribute. Addition, subtraction, and multiplication are supported with :class:`Color` instances. For example:: >>> Color(0.9, 0.9, 0.6) + Saturation(0.1) >>> Color('red') - Saturation(1) >>> Saturation(0.5) * Color('wheat') """ def __repr__(self): return "Saturation({:g})".format(self) class Luma(float): """ Represents the luma of a :class:`Color` for use in transformations. Instances of this class can be constructed directly with a float value, or by querying the :attr:`Color.yuv.y` attribute. Addition, subtraction, and multiplication are supported with :class:`Color` instances. For example:: >>> Color(0, 0, 0) + Luma(0.1) >>> Color('red') * Luma(0.5) """ def __repr__(self): return "Luma({:g})".format(self) colorzero-release-2.0/colorzero/color.py000066400000000000000000001137101402377017200205230ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2016-2021 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause "Defines the main :class:`Color` class of the package." import re from . import conversions as cv, types, attr, deltae, tables, easings # Lots of the methods below use single character parameter names (r for red, y # for luma, etc.); this is is normal and in keeping with most of the referenced # sources # pylint: disable=invalid-name class Color(types.RGB): """ The Color class is a tuple which represents a color as linear red, green, and blue components. The class has a flexible constructor which allows you to create an instance from any built-in color system. There are also explicit constructors for every known system that can convert (directly or indirectly) to linear RGB. For example, an instance of :class:`Color` can be constructed in any of the following ways:: >>> Color('#f00') >>> Color('green') >>> Color(0, 0, 1) >>> Color(h=0, s=1, v=0.5) >>> Color(y=0.4, u=-0.05, v=0.615) The specific forms that the default constructor will accept are enumerated below: .. tabularcolumns:: |p{60mm}|p{70mm}| +------------------------------+------------------------------------------+ | Style | Description | +==============================+==========================================+ | Single scalar parameter | Equivalent to calling | | | :meth:`Color.from_string`, or | | | :meth:`Color.from_rgb24`. | +------------------------------+------------------------------------------+ | Three positional parameters | Equivalent to calling | | or a 3-tuple with no field | :meth:`Color.from_rgb` if all three | | names | parameters are between 0.0 and 1.0, or | | | :meth:`Color.from_rgb_bytes` otherwise. | +------------------------------+ | | Three named parameters, or a | | | 3-tuple with fields | | | "r", "g", "b" | | +------------------------------+ | | Three named parameters, or a | | | 3-tuple with fields | | | "red", "green", "blue" | | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_yuv` if "y" is between | | "y", "u", "v" | 0.0 and 1.0, "u" is between -0.436 and | | | 0.436, and "v" is between -0.615 and | | | 0.615, or :meth:`Color.from_yuv_bytes` | | | otherwise. | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_yiq`. | | "y", "i", "q" | | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_hls`. | | "h", "l", "s" | | +------------------------------+ | | Three named parameters, or a | | | 3-tuple with fields | | | "hue", "lightness", | | | "saturation" | | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_hsv` | | "h", "s", "v" | | +------------------------------+ | | Three named parameters, or a | | | 3-tuple with fields | | | "hue", "saturation", "value" | | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_xyz` | | "x", "y", "z" | | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_lab` | | "l", "a", "b" | | +------------------------------+------------------------------------------+ | Three named parameters, or a | Equivalent to calling | | 3-tuple with fields | :meth:`Color.from_luv` | | "l", "u", "v" | | +------------------------------+------------------------------------------+ If the constructor parameters do not conform to any of the variants in the table above, a :exc:`ValueError` will be raised. Internally, the color is *always* represented as 3 :class:`float` values corresponding to the red, green, and blue components of the color. These values take a value from 0.0 to 1.0 (least to full intensity). The class provides several attributes which can be used to convert one color system into another:: >>> Color('#f00').hls HLS(h=0.0, l=0.5, s=1.0) >>> Color.from_string('green').hue Hue(deg=120.0) >>> Color.from_rgb_bytes(0, 0, 255).yuv YUV(y=0.114, u=0.436, v=-0.10001426533523537) As :class:`Color` derives from tuple, instances are immutable. While this provides the advantage that they can be used in a :class:`set` or as keys of a :class:`dict`, it does mean that colors themselves cannot be *directly* manipulated (e.g. by setting the red component). However, several auxilliary classes in the module provide the ability to perform simple transformations of colors via operators which produce a new :class:`Color` instance. For example, you can add, subtract, and multiply colors directly:: >>> Color('red') + Color('blue') >>> Color('magenta') - Color('red') Values are clipped to ensure the resulting color is still valid:: >>> Color('#ff00ff') + Color('#ff0000') You can wrap numbers in constructors like :class:`Red` (or obtain elements of existing colors), then add, subtract, or multiply them with a :class:`Color`:: >>> Color('red') - Red(0.5) >>> Color('green') + Color('grey').red You can even manipulate non-primary attributes like hue, saturation, and lightness with standard addition, subtraction or multiplication operators:: >>> Color.from_hls(0.5, 0.5, 1.0) >>> Color.from_hls(0.5, 0.5, 1.0) * Lightness(0.8) >>> (Color.from_hls(0.5, 0.5, 1.0) * Lightness(0.8)).hls HLS(h=0.5, l=0.4, s=1.0) In the last example above, a :class:`Color` instance is constructed from HLS (hue, lightness, saturation) values with a lightness of 0.5. This is multiplied by a :class:`Lightness` a value of 0.8 which constructs a new :class:`Color` with the same hue and saturation, but a lightness of 0.4 (0.8 × 0.5). If an instance is converted to a string (with :func:`str`) it will return a string containing the 7-character HTML code for the color (e.g. "#ff0000" for red). As can be seen in the examples above, a similar representation is included for the output of :func:`repr`. The output of :func:`repr` can be customized by assigning values to :attr:`Color.repr_style`. .. _RGB: https://en.wikipedia.org/wiki/RGB_color_space .. _Y'UV: https://en.wikipedia.org/wiki/YUV .. _Y'IQ: https://en.wikipedia.org/wiki/YIQ .. _HLS: https://en.wikipedia.org/wiki/HSL_and_HSV .. _HSV: https://en.wikipedia.org/wiki/HSL_and_HSV .. attribute:: red Return the red value as a :class:`Red` instance .. attribute:: green Return the green value as a :class:`Green` instance .. attribute:: blue Return the blue value as a :class:`Blue` instance .. attribute:: repr_style Specifies the style of output returned when using :func:`repr` against a :class:`Color` instance. This is an attribute of the class, not of instances. For example:: >>> Color('#f00') >>> Color.repr_style = 'html' >>> Color('#f00') Color('#ff0000') The following values are valid: * 'default' - The style shown above * 'term16m' - Similar to the default style, but instead of the HTML style being included, a swatch previewing the color is output. Note that the terminal must support `24-bit color ANSI codes`_ for this to work. * 'term256' - Similar to 'termtrue', but uses the closest color that can be found in the standard 256-color xterm palette. Note that the terminal must support `8-bit color ANSI codes`_ for this to work. * 'html' - Outputs a valid :class:`Color` constructor using the HTML style, e.g. ``Color('#ff99bb')`` * 'rgb' - Outputs a valid :class:`Color` constructor using the floating point RGB values, e.g. ``Color(1, 0.25, 0)`` """ # pylint: disable=too-many-public-methods __slots__ = () repr_style = 'default' def __new__(cls, *args, **kwargs): def from_rgb(r, g, b): "Determine whether bytes or floats are being passed for RGB" if 0.0 <= r <= 1.0 and 0.0 <= g <= 1.0 and 0.0 <= b <= 1.0: return cls.from_rgb(r, g, b) else: return cls.from_rgb_bytes(r, g, b) def from_yuv(y, u, v): "Determine whether bytes or floats are being passed for YUV" if ( 0.0 <= y <= 1.0 and abs(u) <= cv.BT601.Umax and abs(v) <= cv.BT601.Vmax): return cls.from_yuv(y, u, v) else: return cls.from_yuv_bytes(y, u, v) if kwargs: try: # Yes, lambdas are fine here # pylint: disable=unnecessary-lambda return { frozenset('rgb'): from_rgb, frozenset('yuv'): from_yuv, frozenset('yiq'): cls.from_yiq, frozenset('hls'): cls.from_hls, frozenset('hsv'): cls.from_hsv, frozenset('xyz'): cls.from_xyz, frozenset('lab'): cls.from_lab, frozenset('luv'): cls.from_luv, frozenset('cmy'): cls.from_cmy, frozenset('cmyk'): cls.from_cmyk, frozenset(('red', 'green', 'blue')): lambda red, green, blue: from_rgb(red, green, blue), frozenset(('cyan', 'magenta', 'yellow')): lambda cyan, magenta, yellow: cls.from_cmy(cyan, magenta, yellow), frozenset(('cyan', 'magenta', 'yellow', 'black')): lambda cyan, magenta, yellow, black: cls.from_cmyk(cyan, magenta, yellow, black), frozenset(('hue', 'lightness', 'saturation')): lambda hue, lightness, saturation: cls.from_hls(hue, lightness, saturation), frozenset(('hue', 'saturation', 'value')): lambda hue, saturation, value: cls.from_hsv(hue, saturation, value), }[frozenset(kwargs.keys())](**kwargs) except KeyError: pass else: if len(args) == 1: if isinstance(args[0], bytes): spec = args[0].decode('ascii') else: spec = args[0] if isinstance(spec, str): return cls.from_string(spec) elif isinstance(spec, tuple): try: return cls(**spec._asdict()) except AttributeError: if len(spec) == 3: return from_rgb(*spec) elif isinstance(spec, int): return cls.from_rgb24(spec) elif len(args) == 3: r, g, b = args return from_rgb(r, g, b) raise ValueError('Unable to construct Color from provided arguments') @classmethod def from_string(cls, s): """ Construct a :class:`Color` from a 4 or 7 character CSS-like representation (e.g. "#f00" or "#ff0000" for red), or from one of the named colors (e.g. "green" or "wheat") from the `CSS standard`_. Any other string format will result in a :exc:`ValueError`. .. _CSS standard: http://www.w3.org/TR/css3-color/#svg-color """ if s[:1] != '#': s = cv.name_to_html(s) return cls.from_rgb_bytes(*cv.html_to_rgb_bytes(s)) @classmethod def from_rgb(cls, r, g, b): """ Construct a :class:`Color` from three linear `RGB`_ float values between 0.0 and 1.0. """ return super().__new__(cls, cv.clamp_float(r), cv.clamp_float(g), cv.clamp_float(b)) @classmethod def from_rgb24(cls, n): """ Construct a :class:`Color` from an unsigned 24-bit integer number of the form 0x00BBGGRR. """ return cls.from_rgb_bytes(*cv.rgb24_to_rgb_bytes(n)) @classmethod def from_rgb565(cls, n): """ Construct a :class:`Color` from an unsigned 16-bit integer number in RGB565 format. """ return cls.from_rgb(*cv.rgb565_to_rgb(n)) @classmethod def from_rgb_bytes(cls, r, g, b): """ Construct a :class:`Color` from three `RGB`_ byte values between 0 and 255. .. _RGB: https://en.wikipedia.org/wiki/RGB_color_space """ return cls.from_rgb(*cv.rgb_bytes_to_rgb(r, g, b)) @classmethod def from_yuv(cls, y, u, v): """ Construct a :class:`Color` from three `Y'UV`_ float values. The Y value may be between 0.0 and 1.0. U may be between -0.436 and 0.436, while V may be between -0.615 and 0.615. .. _Y'UV: https://en.wikipedia.org/wiki/YUV """ return cls.from_rgb(*cv.yuv_to_rgb(y, u, v)) @classmethod def from_yuv_bytes(cls, y, u, v): """ Construct a :class:`Color` from three `Y'UV`_ byte values between 0 and 255. The U and V values are biased by 128 to prevent negative values as is typical in video applications. The Y value is biased by 16 for the same purpose. .. _Y'UV: https://en.wikipedia.org/wiki/YUV """ return cls.from_rgb_bytes(*cv.yuv_bytes_to_rgb_bytes(y, u, v)) @classmethod def from_yiq(cls, y, i, q): """ Construct a :class:`Color` from three `Y'IQ`_ float values. Y' can be between 0.0 and 1.0, while I and Q can be between -1.0 and 1.0. .. _Y'IQ: https://en.wikipedia.org/wiki/YIQ """ return cls.from_rgb(*cv.yiq_to_rgb(y, i, q)) @classmethod def from_hls(cls, h, l, s): """ Construct a :class:`Color` from `HLS`_ (hue, lightness, saturation) floats between 0.0 and 1.0. .. _HLS: https://en.wikipedia.org/wiki/HSL_and_HSV """ return cls.from_rgb(*cv.hls_to_rgb(h, l, s)) @classmethod def from_hsv(cls, h, s, v): """ Construct a :class:`Color` from `HSV`_ (hue, saturation, value) floats between 0.0 and 1.0. .. _HSV: https://en.wikipedia.org/wiki/HSL_and_HSV """ return cls.from_rgb(*cv.hsv_to_rgb(h, s, v)) @classmethod def from_cmy(cls, c, m, y): """ Construct a :class:`Color` from `CMY`_ (cyan, magenta, yellow) floats between 0.0 and 1.0. .. note:: This conversion uses the basic subtractive method which is not accurate for color reproduction on print devices. See the `Color FAQ`_ for more information. .. _Color FAQ: http://poynton.ca/notes/colour_and_gamma/ColorFAQ.html#RTFToC24 .. _CMY: https://en.wikipedia.org/wiki/CMYK_color_model """ return cls.from_rgb(*cv.cmy_to_rgb(c, m, y)) @classmethod def from_cmyk(cls, c, m, y, k): """ Construct a :class:`Color` from `CMYK`_ (cyan, magenta, yellow, black) floats between 0.0 and 1.0. .. note:: This conversion uses the basic subtractive method which is not accurate for color reproduction on print devices. See the `Color FAQ`_ for more information. .. _Color FAQ: http://poynton.ca/notes/colour_and_gamma/ColorFAQ.html#RTFToC24 .. _CMYK: https://en.wikipedia.org/wiki/CMYK_color_model """ return cls.from_cmy(*cv.cmyk_to_cmy(c, m, y, k)) @classmethod def from_xyz(cls, x, y, z): """ Construct a :class:`Color` from (X, Y, Z) float values representing a color in the `CIE 1931 color space`_. The conversion assumes the sRGB working space with reference white D65. .. _CIE 1931 color space: https://en.wikipedia.org/wiki/CIE_1931_color_space """ return cls.from_rgb(*cv.xyz_to_rgb(x, y, z)) @classmethod def from_lab(cls, l, a, b): """ Construct a :class:`Color` from (L*, a*, b*) float values representing a color in the `CIE Lab color space`_. The conversion assumes the sRGB working space with reference white D65. .. _CIE Lab color space: https://en.wikipedia.org/wiki/Lab_color_space """ return cls.from_xyz(*cv.lab_to_xyz(l, a, b)) @classmethod def from_luv(cls, l, u, v): """ Construct a :class:`Color` from (L*, u*, v*) float values representing a color in the `CIE Luv color space`_. The conversion assumes the sRGB working space with reference white D65. .. _CIE Luv color space: https://en.wikipedia.org/wiki/CIELUV """ return cls.from_xyz(*cv.luv_to_xyz(l, u, v)) def __add__(self, other): if isinstance(other, types.RGB): return Color.from_rgb(self.r + other.r, self.g + other.g, self.b + other.b) elif isinstance(other, (attr.Red, attr.Green, attr.Blue)): r, g, b = self return Color.from_rgb( r + other if isinstance(other, attr.Red) else r, g + other if isinstance(other, attr.Green) else g, b + other if isinstance(other, attr.Blue) else b, ) elif isinstance(other, (attr.Hue, attr.Lightness, attr.Saturation)): h, l, s = self.hls return Color.from_hls( h + other if isinstance(other, attr.Hue) else h, l + other if isinstance(other, attr.Lightness) else l, s + other if isinstance(other, attr.Saturation) else s, ) elif isinstance(other, attr.Luma): y, u, v = self.yuv return Color.from_yuv(y + other, u, v) return NotImplemented def __radd__(self, other): # Addition is commutative if isinstance(other, (types.RGB, attr.Red, attr.Green, attr.Blue, attr.Hue, attr.Lightness, attr.Saturation, attr.Luma)): return self.__add__(other) return NotImplemented def __sub__(self, other): if isinstance(other, types.RGB): return Color.from_rgb(self.r - other.r, self.g - other.g, self.b - other.b) elif isinstance(other, (attr.Red, attr.Green, attr.Blue)): r, g, b = self.rgb return Color.from_rgb( r - other if isinstance(other, attr.Red) else r, g - other if isinstance(other, attr.Green) else g, b - other if isinstance(other, attr.Blue) else b, ) elif isinstance(other, (attr.Hue, attr.Lightness, attr.Saturation)): h, l, s = self.hls return Color.from_hls( h - other if isinstance(other, attr.Hue) else h, l - other if isinstance(other, attr.Lightness) else l, s - other if isinstance(other, attr.Saturation) else s, ) elif isinstance(other, attr.Luma): y, u, v = self.yuv return Color.from_yuv(y - other, u, v) return NotImplemented def __rsub__(self, other): if isinstance(other, (attr.Red, attr.Green, attr.Blue)): r, g, b = self.rgb return Color.from_rgb( other - r if isinstance(other, attr.Red) else 0.0, other - g if isinstance(other, attr.Green) else 0.0, other - b if isinstance(other, attr.Blue) else 0.0, ) return NotImplemented def __mul__(self, other): if isinstance(other, types.RGB): return Color.from_rgb(self.r * other.r, self.g * other.g, self.b * other.b) elif isinstance(other, (attr.Red, attr.Green, attr.Blue)): r, g, b = self return Color.from_rgb( r * other if isinstance(other, attr.Red) else r, g * other if isinstance(other, attr.Green) else g, b * other if isinstance(other, attr.Blue) else b, ) elif isinstance(other, (attr.Hue, attr.Lightness, attr.Saturation)): h, l, s = self.hls return Color.from_hls( h * other if isinstance(other, attr.Hue) else h, l * other if isinstance(other, attr.Lightness) else l, s * other if isinstance(other, attr.Saturation) else s, ) elif isinstance(other, attr.Luma): y, u, v = self.yuv return Color.from_yuv(y * other, u, v) return NotImplemented def __rmul__(self, other): # Multiplication is commutative if isinstance(other, (types.RGB, attr.Red, attr.Green, attr.Blue, attr.Hue, attr.Lightness, attr.Saturation, attr.Luma)): return self.__mul__(other) return NotImplemented _format_re = re.compile( r'^(' r'(?Phtml)|' r'(?Pcss(?Prgb|hsl)?)|' r'(?P[fb])?(?P0|8|256|16[mM])?' r')$') def __format__(self, format_spec): m = Color._format_re.match(format_spec) if not m: raise ValueError( 'Invalid format {:r} for Color'.format(format_spec)) if m.group('html'): return self.html elif m.group('css'): return self._format_css(m.group('cssfmt')) else: return self._format_term(m.group('back'), m.group('term')) def _format_css(self, cssfmt): return { None: lambda self: 'rgb({0:d}, {1:d}, {2:d})'.format( *self.rgb_bytes), 'rgb': lambda self: 'rgb({0:d}, {1:d}, {2:d})'.format( *self.rgb_bytes), 'hsl': lambda self: 'hsl({0:g}deg, {1:g}%, {2:g}%)'.format( self.hls.hue.deg, self.hls.saturation * 100, self.hls.lightness * 100), }[cssfmt](self) def _format_term(self, back, term): if term == '0': args = ({ None: 0, 'f': 39, 'b': 49, }[back],) elif term in (None, '8'): table = tables.DOS_COLORS if back == 'b': code = 40 table = { k: (bold, index) for k, (bold, index) in table.items() if not bold } else: code = 30 try: bold, index = table[self.rgb_bytes] except KeyError: bold, index = sorted( (self.difference(Color.from_rgb_bytes(*color)), bold, index) for color, (bold, index) in table.items() )[0][1:] args = (1,) if bold else () args += (code + index,) elif term == '256': code = 48 if back == 'b' else 38 try: index = tables.XTERM_COLORS[self.rgb_bytes] except KeyError: index = sorted( (self.difference(Color.from_rgb_bytes(*color)), index) for color, index in tables.XTERM_COLORS.items() )[0][1] args = (48 if back == 'b' else 38, 5, index) elif term.lower() == '16m': args = (48 if back == 'b' else 38, 2) + self.rgb_bytes else: assert False # pragma: no cover return '\x1b[' + ';'.join(str(i) for i in args) + 'm' def __str__(self): return self.html def __repr__(self): try: return { 'default': '', 'term16m': '', 'term256': '', 'html': 'Color({self.html!r})', 'rgb': 'Color({self.r:g}, {self.g:g}, {self.b:g})', }[Color.repr_style].format(self=self) except KeyError: raise ValueError( 'invalid repr_style value: {}'.format(Color.repr_style)) @property def html(self): """ Returns the color as a string in HTML #RRGGBB format. """ return cv.rgb_bytes_to_html(*self.rgb_bytes) @property def rgb(self): """ Return a simple 3-tuple of (r, g, b) float values in the range 0.0 <= n <= 1.0. .. note:: The :class:`Color` class can already be treated as such a 3-tuple but for the cases where you want a straight :func:`~collections.namedtuple` this property is available. """ return types.RGB(*self) @property def rgb565(self): """ Returns an unsigned 16-bit integer number representing the color in the RGB565 encoding. """ return cv.rgb_to_rgb565(*self) @property def rgb_bytes(self): """ Returns a 3-tuple of (red, green, blue) byte values. """ return cv.rgb_to_rgb_bytes(*self) @property def yuv(self): """ Returns a 3-tuple of (y, u, v) float values; Y values can be between 0.0 and 1.0, U values are between -0.436 and 0.436, and V values are between -0.615 and 0.615. """ return cv.rgb_to_yuv(*self) @property def yuv_bytes(self): """ Returns a 3-tuple of (y, u, v) byte values. Y values are biased by 16 in the result to prevent negatives. U and V values are biased by 128 for the same purpose. """ return cv.rgb_bytes_to_yuv_bytes(*self.rgb_bytes) @property def yiq(self): """ Returns a 3-tuple of (y, i, q) float values; y values can be between 0.0 and 1.0, whilst i and q values can be between -1.0 and 1.0. """ return cv.rgb_to_yiq(*self) @property def xyz(self): """ Returns a 3-tuple of (X, Y, Z) float values representing the color in the `CIE 1931 color space`_. The conversion assumes the sRGB working space, with reference white D65. .. _CIE 1931 color space: https://en.wikipedia.org/wiki/CIE_1931_color_space """ return cv.rgb_to_xyz(*self) @property def lab(self): """ Returns a 3-tuple of (L*, a*, b*) float values representing the color in the `CIE Lab color space`_ with the `D65 standard illuminant`_. .. _CIE Lab color space: https://en.wikipedia.org/wiki/Lab_color_space .. _D65 standard illuminant: https://en.wikipedia.org/wiki/Illuminant_D65 """ return cv.xyz_to_lab(*self.xyz) @property def luv(self): """ Returns a 3-tuple of (L*, u*, v*) float values representing the color in the `CIE Luv color space`_ with the `D65 standard illuminant`_. .. _CIE Luv color space: https://en.wikipedia.org/wiki/CIELUV """ return cv.xyz_to_luv(*self.xyz) @property def hls(self): """ Returns a 3-tuple of (hue, lightness, saturation) float values (between 0.0 and 1.0). """ return cv.rgb_to_hls(*self) @property def hsv(self): """ Returns a 3-tuple of (hue, saturation, value) float values (between 0.0 and 1.0). """ return cv.rgb_to_hsv(*self) @property def cmy(self): """ Returns a 3-tuple of (cyan, magenta, yellow) float values (between 0.0 and 1.0). .. note:: This conversion uses the basic subtractive method which is not accurate for color reproduction on print devices. See the `Color FAQ`_ for more information. .. _Color FAQ: http://poynton.ca/notes/colour_and_gamma/ColorFAQ.html#RTFToC24 """ return cv.rgb_to_cmy(*self) @property def cmyk(self): """ Returns a 4-tuple of (cyan, magenta, yellow, black) float values (between 0.0 and 1.0). .. note:: This conversion uses the basic subtractive method which is not accurate for color reproduction on print devices. See the `Color FAQ`_ for more information. .. _Color FAQ: http://poynton.ca/notes/colour_and_gamma/ColorFAQ.html#RTFToC24 """ return cv.cmy_to_cmyk(*self.cmy) @property def hue(self): """ Returns the hue of the color as a :class:`Hue` instance which can be used in operations with other :class:`Color` instances. """ return attr.Hue(self.hls[0]) @property def lightness(self): """ Returns the lightness of the color as a :class:`Lightness` instance which can be used in operations with other :class:`Color` instances. """ return attr.Lightness(self.hls[1]) @property def saturation(self): """ Returns the saturation of the color as a :class:`Saturation` instance which can be used in operations with other :class:`Color` instances. """ return attr.Saturation(self.hls[2]) @property def luma(self): """ Returns the `luma`_ of the color as a :class:`Luma` instance which can be used in operations with other :class:`Color` instances. .. _luma: https://en.wikipedia.org/wiki/Luma_(video) """ return attr.Luma(self.yuv[0]) def difference(self, other, method='euclid'): """ Determines the difference between this color and *other* using the specified *method*. :param Color other: The color to compare this color to. :param str method: The algorithm to use in the comparison. Valid values are: * 'euclid' - This is the default method. Calculate the `Euclidian distance`_. This is by far the fastest method, but also the least accurate in terms of human perception. * 'cie1976' - Use the `CIE 1976`_ formula for calculating the difference between two colors in CIE Lab space. * 'cie1994g' - Use the `CIE 1994`_ formula with the "graphic arts" bias for calculating the difference. * 'cie1994t' - Use the `CIE 1994`_ forumula with the "textiles" bias for calculating the difference. * 'ciede2000' - Use the `CIEDE 2000`_ formula for calculating the difference. :returns: A :class:`float` indicating how different the two colors are. Note that the Euclidian distance will be significantly different to the other calculations; effectively this just measures the distance between the two colors by treating them as coordinates in a three dimensional Euclidian space. All other methods are means of calculating a `Delta E`_ value in which 2.3 is considered a `just-noticeable difference`_ (JND). For example:: >>> Color('red').difference(Color('red')) 0.0 >>> Color('red').difference(Color('red'), method='cie1976') 0.0 >>> Color('red').difference(Color('#900')) 0.4 >>> Color('red').difference(Color('#900'), method='cie1976') 40.17063087142142 >>> Color('red').difference(Color('#900'), method='ciede2000') 21.078146289272155 >>> Color('red').difference(Color('blue')) 1.4142135623730951 >>> Color('red').difference(Color('blue'), method='cie1976') 176.31403908880046 .. note:: Instead of using this method, you may wish to simply use the various difference functions (:func:`euclid`, :func:`cie1976`, etc.) directly. .. _Delta E: https://en.wikipedia.org/wiki/Color_difference .. _just-noticeable difference: https://en.wikipedia.org/wiki/Just-noticeable_difference .. _Euclidian distance: https://en.wikipedia.org/wiki/Euclidean_distance .. _CIE 1976: https://en.wikipedia.org/wiki/Color_difference#CIE76 .. _CIE 1994: https://en.wikipedia.org/wiki/Color_difference#CIE94 .. _CIEDE 2000: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000 """ if isinstance(method, bytes): method = method.decode('ascii') try: fn = getattr(deltae, method) except AttributeError: raise ValueError('invalid method: {}'.format(method)) else: if method.startswith('cie'): return fn(self.lab, other.lab) else: return fn(self, other) def gradient(self, other, steps=10, easing=easings.linear): """ Returns a generator which fades between this color and *other* in the specified number of *steps*. :param Color other: The color that will end the gradient (with the color the method is called upon starting the gradient) :param int steps: The unqiue number of colors to include in the generated gradient. Defaults to 10 if unspecified. :param callable easing: A function which controls the speed of the progression. If specified, if must be a function which takes a single parameter, the number of *steps*, and yields a sequence of values between 0.0 (representing the start of the gradient) and 1.0 (representing the end). The default is :func:`linear`. :return: A generator yielding *steps* :class:`Color` instances which fade from this color to *other*. For example:: >>> Color.repr_style = 'html' >>> print('\\n'.join( ... repr(c) for c in ... Color('red').gradient(Color('green')) ... )) Color('#ff0000') Color('#e30e00') Color('#c61c00') Color('#aa2b00') Color('#8e3900') Color('#714700') Color('#555500') Color('#396400') Color('#1c7200') Color('#008000') .. versionadded:: 1.1 """ if steps < 2: raise ValueError('steps must be >= 2') # NOTE: Can't simply subtract self from other here, as the result will # be clamped and we want the actual result. delta = types.RGB(*( other_i - self_i for self_i, other_i in zip(self, other) )) for t in easing(steps): yield self + types.RGB(*(delta_i * t for delta_i in delta)) colorzero-release-2.0/colorzero/conversions.py000066400000000000000000000261171402377017200217610ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2016-2021 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause """ Defines all conversion functions used by colorzero to convert between the various color systems implemented. References used in the development of these routines are as follows: * `Charles Poynton's Color FAQ`_ * `Bruce Lindbloom's Color Equations`_ * `RGB color space`_ article from Wikipedia * `SRGB`_ article from Wikipedia * `YUV`_ article from Wikipedia * `YIQ`_ article from Wikipedia * `HSL and HSV`_ article from Wikipedia * `CIE 1931 color space`_ article from Wikipedia .. _RGB color space: https://en.wikipedia.org/wiki/RGB_color_space .. _SRGB: https://en.wikipedia.org/wiki/SRGB .. _YUV: https://en.wikipedia.org/wiki/YUV .. _YIQ: https://en.wikipedia.org/wiki/YIQ .. _HSL and HSV: https://en.wikipedia.org/wiki/HSL_and_HSV .. _CIE 1931 color space: https://en.wikipedia.org/wiki/CIE_1931_color_space .. _Charles Poynton's Color FAQ: http://www.poynton.com/notes/colour_and_gamma/ColorFAQ.html .. _Bruce Lindbloom's Color Equations: https://www.brucelindbloom.com/ """ import colorsys from collections import namedtuple from fractions import Fraction from .tables import NAMED_COLORS from .types import RGB, YIQ, YUV, CMY, CMYK, HLS, HSV, XYZ, Luv, Lab # Lots of the conversion functions use single character parameter names and # variables internally; this is is normal and in keeping with most of the # referenced sources # pylint: disable=invalid-name # Utility functions and constants ############################################ def clamp_float(v): "Clamp *v* to the range 0.0 to 1.0 inclusive" return max(0.0, min(1.0, v)) def clamp_bytes(v): "Clamp *v* to the range 0 to 255 inclusive" return max(0, min(255, v)) def to_srgb(c): "Convert a linear RGB value (0..1) to the sRGB color space" return 12.92 * c if c <= 0.0031308 else (1.055 * c ** (1 / 2.4) - 0.055) def from_srgb(c): "Convert an RGB value from the sRGB color space to linear RGB" return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 def xyz_to_uv(x, y, z): "Calculate the U, V values from an XYZ color" d = x + 15 * y + 3 * z return (0, 0) if d == 0 else (4 * x / d, 9 * y / d) def matrix_mult(m, n): "Generator function that multiplies matrices *m* and *n*" return ( sum(mval * nval for mval, nval in zip(mrow, n)) for mrow in m ) class YUVCoefficients(namedtuple('YUVCoefficients', ( 'Wr', 'Wg', 'Wb', 'Umax', 'Vmax', 'U', 'V', 'Rv', 'Gu', 'Gv', 'Bu'))): "Represents coefficients for the BT.601 and BT.709 standards." def __new__(cls, Umax=0.436, Vmax=0.615, **kwargs): try: Wr = kwargs['Wr'] Wb = kwargs['Wb'] except KeyError as e: raise TypeError('YUVCoefficients() missing required keyword ' 'argument: {e:s}'.format(e=e)) Wg = (1 - Wr - Wb) U = Umax / (1 - Wb) V = Vmax / (1 - Wr) Rv = (1 - Wr) / Vmax Bu = (1 - Wb) / Umax Gu = (Wb * (1 - Wb)) / (Umax * Wg) Gv = (Wr * (1 - Wr)) / (Vmax * Wg) return super(YUVCoefficients, cls).__new__(cls, Wr, Wg, Wb, Umax, Vmax, U, V, Rv, Gu, Gv, Bu) BT601 = YUVCoefficients(Wr=0.299, Wb=0.114) BT709 = YUVCoefficients(Wr=0.2126, Wb=0.0722) SMPTE240M = YUVCoefficients(Wr=0.212, Wb=0.087) # TODO define some API to use these in Color # The standard illuminants in the CIE XYZ space D50 = XYZ(0.966797, 1.0, 0.825188) D65 = XYZ(0.95047, 1.0, 1.08883) # TODO define some more of these and figure out some API to use them in Color # TODO what about standard observers? color temperature? # Conversion functions ####################################################### def rgb_to_yiq(r, g, b): "Convert a linear RGB color to YIQ" # Coefficients from Python 3.4+ y = 0.30 * r + 0.59 * g + 0.11 * b i = 0.74 * (r - y) - 0.27 * (b - y) q = 0.48 * (r - y) + 0.41 * (b - y) return YIQ(y, i, q) def yiq_to_rgb(y, i, q): "Convert a YIQ color to linear RGB" # Coefficients from Python 3.4+ return RGB( clamp_float(y + 0.9468822170900693 * i + 0.6235565819861433 * q), clamp_float(y - 0.27478764629897834 * i - 0.6356910791873801 * q), clamp_float(y - 1.1085450346420322 * i + 1.7090069284064666 * q), ) def rgb_to_hls(r, g, b): "Convert a linear RGB color to HLS" return HLS(*colorsys.rgb_to_hls(r, g, b)) def hls_to_rgb(h, l, s): "Convert an HLS color to linear RGB" return RGB(*colorsys.hls_to_rgb(h, l, s)) def rgb_to_hsv(r, g, b): "Convert a linear RGB color to HSV" return HSV(*colorsys.rgb_to_hsv(r, g, b)) def hsv_to_rgb(h, s, v): "Convert an HSV color to linear RGB" return RGB(*colorsys.hsv_to_rgb(h, s, v)) def rgb_to_rgb_bytes(r, g, b): "Convert a linear RGB color to RGB888" return RGB(int(round(r * 255)), int(round(g * 255)), int(round(b * 255))) def rgb_bytes_to_rgb(r, g, b): "Convert an RGB888 color to linear RGB" return RGB(r / 255, g / 255, b / 255) def rgb_bytes_to_html(r, g, b): "Convert RGB888 to the HTML representation" return '#{0:02x}{1:02x}{2:02x}'.format(r, g, b) def rgb_bytes_to_rgb24(r, g, b): "Convert RGB888 to RGB24" return (b << 16) | (g << 8) | r def rgb24_to_rgb_bytes(n): "Convert RGB24 to RGB888" return RGB(n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF) def html_to_rgb_bytes(html): "Convert the HTML color representation to RGB888" if html.startswith('#'): try: if len(html) == 7: return RGB( int(html[1:3], base=16), int(html[3:5], base=16), int(html[5:7], base=16) ) elif len(html) == 4: return RGB( int(html[1:2], base=16) * 0x11, int(html[2:3], base=16) * 0x11, int(html[3:4], base=16) * 0x11 ) except ValueError: pass raise ValueError( '{:s} is not a valid HTML color specification'.format(html)) def name_to_html(name): "Convert a named color to the HTML representation" try: return NAMED_COLORS[name] except KeyError: raise ValueError('invalid color name {:s}'.format(name)) def rgb_to_rgb565(r, g, b): "Convert linear RGB to RGB565" return ( (int(r * 0xF800) & 0xF800) | (int(g * 0x07E0) & 0x07E0) | (int(b * 0x001F) & 0x001F) ) def rgb565_to_rgb(rgb565): "Convert RGB565 to linear RGB" r = (rgb565 & 0xF800) / 0xF800 g = (rgb565 & 0x07E0) / 0x07E0 b = (rgb565 & 0x001F) / 0x001F return RGB(r, g, b) def rgb_to_yuv(r, g, b, std=BT601): """ Convert linear RGB to Y'CbCr using the specified coefficients (the default coefficients are from BT.601) """ y = std.Wr * r + std.Wg * g + std.Wb * b return YUV(y, std.U * (b - y), std.V * (r - y)) def yuv_to_rgb(y, u, v, std=BT601): """ Convert Y'CbCr to linear RGB using the specified coefficients (the default coefficients are from BT.601) """ return RGB( clamp_float(y + std.Rv * v), clamp_float(y - std.Gu * u - std.Gv * v), clamp_float(y + std.Bu * u), ) def rgb_bytes_to_yuv_bytes(r, g, b): "Convert RGB888 to YUV444 bytes using studio swing from BT.601" # pylint: disable=bad-whitespace return YUV( (( 66 * r + 129 * g + 25 * b + 128) >> 8) + 16, ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128, ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128, ) def yuv_bytes_to_rgb_bytes(y, u, v): "Convert YUV444 bytes to RGB888 using studio swing from BT.601" c = y - 16 d = u - 128 e = v - 128 return RGB( clamp_bytes((298 * c + 409 * e + 128) >> 8), clamp_bytes((298 * c - 100 * d - 208 * e + 128) >> 8), clamp_bytes((298 * c + 516 * d + 128) >> 8), ) def rgb_to_cmy(r, g, b): "Convert linear RGB to CMY using the subtractive method" return CMY(1 - r, 1 - g, 1 - b) def cmy_to_rgb(c, m, y): "Convert CMY to linear RGB using the subtractive method" return RGB(1 - c, 1 - m, 1 - y) def cmy_to_cmyk(c, m, y): "Calculate the black component of CMY to convert to CMYK" k = min(c, m, y) if k == 1.0: return CMYK(0.0, 0.0, 0.0, 1.0) else: d = 1.0 - k return CMYK((c - k) / d, (m - k) / d, (y - k) / d, k) def cmyk_to_cmy(c, m, y, k): "Remove the black component from CMYK to yield CMY" n = 1 - k return CMY(c * n + k, m * n + k, y * n + k) def rgb_to_xyz(r, g, b): """ Convert linear RGB to CIE XYZ representation. RGB is assumed to be sRGB and conversion uses D65 as reference white. """ return XYZ(*matrix_mult( ((0.4124564, 0.3575761, 0.1804375), (0.2126729, 0.7151522, 0.0721750), (0.0193339, 0.1191920, 0.9503041)), (from_srgb(r), from_srgb(g), from_srgb(b)) )) def xyz_to_rgb(x, y, z): """ Convert CIE XYZ representation to linear RGB. sRGB is used as the output color space, and D65 as reference white. """ # pylint: disable=bad-whitespace m = matrix_mult( (( 3.2404542, -1.5371385, -0.4985314), (-0.9692660, 1.8760108, 0.0415560), ( 0.0556434, -0.2040259, 1.0572252)), (x, y, z) ) return RGB(*(to_srgb(c) for c in m)) def luv_to_xyz(l, u, v, white=D65): "Convert CIE L*u*v* to CIE XYZ representation" if l == 0: return XYZ(0, 0, 0) uw, vw = xyz_to_uv(*white) u_prime = u / (13 * l) + uw v_prime = v / (13 * l) + vw y = white.y * ( l * Fraction(3, 29) ** 3 if l <= 8 else ((l + 16) / 116) ** 3 ) return XYZ( y * (9 * u_prime) / (4 * v_prime), y, y * (12 - 3 * u_prime - 20 * v_prime) / (4 * v_prime), ) def xyz_to_luv(x, y, z, white=D65): "Convert CIE XYZ to CIE L*u*v* representation" uw, vw = xyz_to_uv(*white) u, v = xyz_to_uv(x, y, z) K = Fraction(29, 3) ** 3 e = Fraction(6, 29) ** 3 y_prime = y / white.y L = 116 * y_prime ** Fraction(1, 3) - 16 if y_prime > e else K * y_prime return Luv( L, 13 * L * (u - uw), 13 * L * (v - vw), ) def lab_to_xyz(l, a, b, white=D65): "Convert CIE L*a*b* to CIE XYZ representation" theta = Fraction(6, 29) fy = (l + 16) / 116 fx = fy + a / 500 fz = fy - b / 200 xyz = ( n ** 3 if n > theta else 3 * theta ** 2 * (n - Fraction(4, 29)) for n in (fx, fy, fz) ) return XYZ(*(n * m for n, m in zip(xyz, white))) def xyz_to_lab(x, y, z, white=D65): "Convert CIE XYZ to CIE L*a*b* representation" theta = Fraction(6, 29) x, y, z = (n / m for n, m in zip((x, y, z), white)) fx, fy, fz = ( t ** Fraction(1, 3) if t > theta ** 3 else t / (3 * theta ** 2) + Fraction(4, 29) for t in (x, y, z) ) return Lab(116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)) colorzero-release-2.0/colorzero/deltae.py000066400000000000000000000130511402377017200206400ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2018 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause "Defines the various algorithms for :meth:`Color.difference`." from math import sqrt, atan2, degrees, radians, sin, cos, exp # Lots of the delta-e functions use single character parameter names and # variables internally; this is is normal and in keeping with most of the # referenced sources # pylint: disable=invalid-name def euclid(color1, color2): """ Calculates color difference as a simple `Euclidean distance`_ by treating the three components as spatial dimensions. .. note:: This function will return considerably different values to the other difference functions. In particular, the maximum "difference" will be :math:`\\sqrt{3}` which is much smaller than the output of the CIE functions. .. _Euclidean distance: https://en.wikipedia.org/wiki/Euclidean_distance """ return sqrt(sum((e1 - e2) ** 2 for e1, e2 in zip(color1, color2))) def cie1976(color1, color2): """ Calculates color difference according to the `CIE 1976`_ formula. Effectively this is the Euclidean formula, but with CIE L*a*b* components instead of RGB. .. _CIE 1976: https://en.wikipedia.org/wiki/Color_difference#CIE76 """ return sqrt(sum((e1 - e2) ** 2 for e1, e2 in zip(color1, color2))) def cie1994(color1, color2, method): """ Calculates color difference according to the `CIE 1994`_ formula. The *method* can be either "cie1994g" for the "graphical" biases, or "cie1994t" for the "textile" biases. The CIE1994 is also basically the Euclidean formula (with biases) but in CIE L*C*H* space. .. _CIE 1994: https://en.wikipedia.org/wiki/Color_difference#CIE94 """ C1 = sqrt(color1.a ** 2 + color1.b ** 2) C2 = sqrt(color2.a ** 2 + color2.b ** 2) dL = color1.l - color2.l dC = C1 - C2 # Don't bother with the sqrt here as due to limited float precision # we can wind up with a domain error (because the value is ever so # slightly negative - try it with black'n'white for an example), and we're # just going to square the result in the final equation anyway dH2 = (color1.a - color2.a) ** 2 + (color1.b - color2.b) ** 2 - dC ** 2 kL, K1, K2 = { 'cie1994g': (1, 0.045, 0.015), 'cie1994t': (2, 0.048, 0.014), }[method] SC = 1 + K1 * C1 SH = 1 + K2 * C1 return sqrt( (dL / kL) ** 2 + (dC / SC) ** 2 + (dH2 / SH ** 2) ) def cie1994g(color1, color2): """ Calculates color difference according to the `CIE 1994`_ formula with the "textile" bias. See :func:`cie1994` for further information. .. _CIE 1994: https://en.wikipedia.org/wiki/Color_difference#CIE94 """ return cie1994(color1, color2, 'cie1994g') def cie1994t(color1, color2): """ Calculates color difference according to the `CIE 1994`_ formula with the "graphics" bias. See :func:`cie1994` for further information. .. _CIE 1994: https://en.wikipedia.org/wiki/Color_difference#CIE94 """ return cie1994(color1, color2, 'cie1994t') def ciede2000(color1, color2): """ Calculates color difference according to the `CIEDE 2000`_ formula. This is the most accurate algorithm currently implemented but also the most complex and slowest. Like CIE1994 it is largely based in CIE L*C*h* space, but with several modifications to account for perceptual uniformity flaws. .. _CIEDE 2000: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000 """ # See WP article and Sharma 2005 for important implementation notes: # http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf # # Yes, there's lots of locals; but this is easiest to understand as it's a # near straight translation of the math # pylint: disable=too-many-locals C_ = ( sqrt(color1.a ** 2 + color1.b ** 2) + sqrt(color2.a ** 2 + color2.b ** 2) ) / 2 G = (1 - sqrt(C_ ** 7 / (C_ ** 7 + 25 ** 7))) / 2 a1_prime = (1 + G) * color1.a a2_prime = (1 + G) * color2.a C1_prime = sqrt(a1_prime ** 2 + color1.b ** 2) C2_prime = sqrt(a2_prime ** 2 + color2.b ** 2) L_ = (color1.l + color2.l) / 2 C_ = (C1_prime + C2_prime) / 2 h1 = ( 0.0 if color1.b == a1_prime == 0 else degrees(atan2(color1.b, a1_prime)) % 360 ) h2 = ( 0.0 if color2.b == a2_prime == 0 else degrees(atan2(color2.b, a2_prime)) % 360 ) if C1_prime * C2_prime == 0.0: dh = 0.0 h_ = h1 + h2 elif abs(h1 - h2) <= 180: dh = h2 - h1 h_ = (h1 + h2) / 2 else: if h2 > h1: dh = h2 - h1 - 360 else: dh = h2 - h1 + 360 if h1 + h2 >= 360: h_ = (h1 + h2 - 360) / 2 else: h_ = (h1 + h2 + 360) / 2 dL = color2.l - color1.l dC = C2_prime - C1_prime dH = 2 * sqrt(C1_prime * C2_prime) * sin(radians(dh / 2)) T = ( 1 - 0.17 * cos(radians(h_ - 30)) + 0.24 * cos(radians(2 * h_)) + 0.32 * cos(radians(3 * h_ + 6)) - 0.20 * cos(radians(4 * h_ - 63)) ) SL = 1 + (0.015 * (L_ - 50) ** 2) / sqrt(20 + (L_ - 50) ** 2) SC = 1 + 0.045 * C_ SH = 1 + 0.015 * C_ * T RT = ( -2 * sqrt(C_ ** 7 / (C_ ** 7 + 25 ** 7)) * sin(radians(60 * exp(-(((h_ - 275) / 25) ** 2)))) ) return sqrt( (dL / SL) ** 2 + (dC / SC) ** 2 + (dH / SH) ** 2 + RT * (dC / SC) * (dH / SH) ) colorzero-release-2.0/colorzero/easings.py000066400000000000000000000015651402377017200210420ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2018 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause "Defines various easing functions for :meth:`Color.gradient`." def linear(steps): "Linear easing function; yields *steps* values between 0.0 and 1.0" for t in range(steps): yield t / (steps - 1) def ease_in(steps): "Quadratic ease-in function; yields *steps* values between 0.0 and 1.0" for t in linear(steps): yield t ** 2 def ease_out(steps): "Quadratic ease-out function; yields *steps* values between 0.0 and 1.0" for t in linear(steps): yield t * (2 - t) def ease_in_out(steps): "Quadratic ease-in-out function; yields *steps* values between 0.0 and 1.0" for t in linear(steps): yield 2 * t * t if t < 0.5 else (4 - 2 * t) * t - 1 colorzero-release-2.0/colorzero/tables.py000066400000000000000000000331041402377017200206550ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2016-2018 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause """ Defines the available color names, derived from the `CSS Color Module`_ Level 3 Specification, section 4.3, along with tables for the original DOS colors, and XTerm colors (for :func:`format` output). .. _CSS Color Module: http://www.w3.org/TR/css3-color/#svg-color """ # I like my spacing, dammit! # pylint: disable=bad-whitespace,bad-continuation def _transpose(table): # Swap keys and values in a dict, but in the case of duplicated keys, use # the first encountered instead of the last result = {} for k, v in table.items(): result.setdefault(v, k) return result DOS_COLORS = _transpose({ # Bold, Index: (R, G, B), (False, 0): (0, 0, 0), (False, 1): (128, 0, 0), (False, 2): (0, 128, 0), (False, 3): (128, 128, 0), (False, 4): (0, 0, 128), (False, 5): (128, 0, 128), (False, 6): (0, 128, 128), (False, 7): (192, 192, 192), (True, 0): (128, 128, 128), (True, 1): (255, 0, 0), (True, 2): (0, 255, 0), (True, 3): (255, 255, 0), (True, 4): (0, 0, 255), (True, 5): (255, 0, 255), (True, 6): (0, 255, 255), (True, 7): (255, 255, 255), }) XTERM_COLORS = _transpose({ # Index: (R, G, B) 0: (0, 0, 0), 1: (128, 0, 0), 2: (0, 128, 0), 3: (128, 128, 0), 4: (0, 0, 128), 5: (128, 0, 128), 6: (0, 128, 128), 7: (192, 192, 192), 8: (128, 128, 128), 9: (255, 0, 0), 10: (0, 255, 0), 11: (255, 255, 0), 12: (0, 0, 255), 13: (255, 0, 255), 14: (0, 255, 255), 15: (255, 255, 255), 16: (0, 0, 0), 17: (0, 0, 95), 18: (0, 0, 135), 19: (0, 0, 175), 20: (0, 0, 215), 21: (0, 0, 255), 22: (0, 95, 0), 23: (0, 95, 95), 24: (0, 95, 135), 25: (0, 95, 175), 26: (0, 95, 215), 27: (0, 95, 255), 28: (0, 135, 0), 29: (0, 135, 95), 30: (0, 135, 135), 31: (0, 135, 175), 32: (0, 135, 215), 33: (0, 135, 255), 34: (0, 175, 0), 35: (0, 175, 95), 36: (0, 175, 135), 37: (0, 175, 175), 38: (0, 175, 215), 39: (0, 175, 255), 40: (0, 215, 0), 41: (0, 215, 95), 42: (0, 215, 135), 43: (0, 215, 175), 44: (0, 215, 215), 45: (0, 215, 255), 46: (0, 255, 0), 47: (0, 255, 95), 48: (0, 255, 135), 49: (0, 255, 175), 50: (0, 255, 215), 51: (0, 255, 255), 52: (95, 0, 0), 53: (95, 0, 95), 54: (95, 0, 135), 55: (95, 0, 175), 56: (95, 0, 215), 57: (95, 0, 255), 58: (95, 95, 0), 59: (95, 95, 95), 60: (95, 95, 135), 61: (95, 95, 175), 62: (95, 95, 215), 63: (95, 95, 255), 64: (95, 135, 0), 65: (95, 135, 95), 66: (95, 135, 135), 67: (95, 135, 175), 68: (95, 135, 215), 69: (95, 135, 255), 70: (95, 175, 0), 71: (95, 175, 95), 72: (95, 175, 135), 73: (95, 175, 175), 74: (95, 175, 215), 75: (95, 175, 255), 76: (95, 215, 0), 77: (95, 215, 95), 78: (95, 215, 135), 79: (95, 215, 175), 80: (95, 215, 215), 81: (95, 215, 255), 82: (95, 255, 0), 83: (95, 255, 95), 84: (95, 255, 135), 85: (95, 255, 175), 86: (95, 255, 215), 87: (95, 255, 255), 88: (135, 0, 0), 89: (135, 0, 95), 90: (135, 0, 135), 91: (135, 0, 175), 92: (135, 0, 215), 93: (135, 0, 255), 94: (135, 95, 0), 95: (135, 95, 95), 96: (135, 95, 135), 97: (135, 95, 175), 98: (135, 95, 215), 99: (135, 95, 255), 100: (135, 135, 0), 101: (135, 135, 95), 102: (135, 135, 135), 103: (135, 135, 175), 104: (135, 135, 215), 105: (135, 135, 255), 106: (135, 175, 0), 107: (135, 175, 95), 108: (135, 175, 135), 109: (135, 175, 175), 110: (135, 175, 215), 111: (135, 175, 255), 112: (135, 215, 0), 113: (135, 215, 95), 114: (135, 215, 135), 115: (135, 215, 175), 116: (135, 215, 215), 117: (135, 215, 255), 118: (135, 255, 0), 119: (135, 255, 95), 120: (135, 255, 135), 121: (135, 255, 175), 122: (135, 255, 215), 123: (135, 255, 255), 124: (175, 0, 0), 125: (175, 0, 95), 126: (175, 0, 135), 127: (175, 0, 175), 128: (175, 0, 215), 129: (175, 0, 255), 130: (175, 95, 0), 131: (175, 95, 95), 132: (175, 95, 135), 133: (175, 95, 175), 134: (175, 95, 215), 135: (175, 95, 255), 136: (175, 135, 0), 137: (175, 135, 95), 138: (175, 135, 135), 139: (175, 135, 175), 140: (175, 135, 215), 141: (175, 135, 255), 142: (175, 175, 0), 143: (175, 175, 95), 144: (175, 175, 135), 145: (175, 175, 175), 146: (175, 175, 215), 147: (175, 175, 255), 148: (175, 215, 0), 149: (175, 215, 95), 150: (175, 215, 135), 151: (175, 215, 175), 152: (175, 215, 215), 153: (175, 215, 255), 154: (175, 255, 0), 155: (175, 255, 95), 156: (175, 255, 135), 157: (175, 255, 175), 158: (175, 255, 215), 159: (175, 255, 255), 160: (215, 0, 0), 161: (215, 0, 95), 162: (215, 0, 135), 163: (215, 0, 175), 164: (215, 0, 215), 165: (215, 0, 255), 166: (215, 95, 0), 167: (215, 95, 95), 168: (215, 95, 135), 169: (215, 95, 175), 170: (215, 95, 215), 171: (215, 95, 255), 172: (215, 135, 0), 173: (215, 135, 95), 174: (215, 135, 135), 175: (215, 135, 175), 176: (215, 135, 215), 177: (215, 135, 255), 178: (215, 175, 0), 179: (215, 175, 95), 180: (215, 175, 135), 181: (215, 175, 175), 182: (215, 175, 215), 183: (215, 175, 255), 184: (215, 215, 0), 185: (215, 215, 95), 186: (215, 215, 135), 187: (215, 215, 175), 188: (215, 215, 215), 189: (215, 215, 255), 190: (215, 255, 0), 191: (215, 255, 95), 192: (215, 255, 135), 193: (215, 255, 175), 194: (215, 255, 215), 195: (215, 255, 255), 196: (255, 0, 0), 197: (255, 0, 95), 198: (255, 0, 135), 199: (255, 0, 175), 200: (255, 0, 215), 201: (255, 0, 255), 202: (255, 95, 0), 203: (255, 95, 95), 204: (255, 95, 135), 205: (255, 95, 175), 206: (255, 95, 215), 207: (255, 95, 255), 208: (255, 135, 0), 209: (255, 135, 95), 210: (255, 135, 135), 211: (255, 135, 175), 212: (255, 135, 215), 213: (255, 135, 255), 214: (255, 175, 0), 215: (255, 175, 95), 216: (255, 175, 135), 217: (255, 175, 175), 218: (255, 175, 215), 219: (255, 175, 255), 220: (255, 215, 0), 221: (255, 215, 95), 222: (255, 215, 135), 223: (255, 215, 175), 224: (255, 215, 215), 225: (255, 215, 255), 226: (255, 255, 0), 227: (255, 255, 95), 228: (255, 255, 135), 229: (255, 255, 175), 230: (255, 255, 215), 231: (255, 255, 255), 232: (8, 8, 8), 233: (18, 18, 18), 234: (28, 28, 28), 235: (38, 38, 38), 236: (48, 48, 48), 237: (58, 58, 58), 238: (68, 68, 68), 239: (78, 78, 78), 240: (88, 88, 88), 241: (98, 98, 98), 242: (108, 108, 108), 243: (118, 118, 118), 244: (128, 128, 128), 245: (138, 138, 138), 246: (148, 148, 148), 247: (158, 158, 158), 248: (168, 168, 168), 249: (178, 178, 178), 250: (188, 188, 188), 251: (198, 198, 198), 252: (208, 208, 208), 253: (218, 218, 218), 254: (228, 228, 228), 255: (238, 238, 238), }) NAMED_COLORS = { 'aliceblue': '#f0f8ff', 'antiquewhite': '#faebd7', 'aqua': '#00ffff', 'aquamarine': '#7fffd4', 'azure': '#f0ffff', 'beige': '#f5f5dc', 'bisque': '#ffe4c4', 'black': '#000000', 'blanchedalmond': '#ffebcd', 'blue': '#0000ff', 'blueviolet': '#8a2be2', 'brown': '#a52a2a', 'burlywood': '#deb887', 'cadetblue': '#5f9ea0', 'chartreuse': '#7fff00', 'chocolate': '#d2691e', 'coral': '#ff7f50', 'cornflowerblue': '#6495ed', 'cornsilk': '#fff8dc', 'crimson': '#dc143c', 'cyan': '#00ffff', 'darkblue': '#00008b', 'darkcyan': '#008b8b', 'darkgoldenrod': '#b8860b', 'darkgray': '#a9a9a9', 'darkgreen': '#006400', 'darkgrey': '#a9a9a9', 'darkkhaki': '#bdb76b', 'darkmagenta': '#8b008b', 'darkolivegreen': '#556b2f', 'darkorange': '#ff8c00', 'darkorchid': '#9932cc', 'darkred': '#8b0000', 'darksalmon': '#e9967a', 'darkseagreen': '#8fbc8f', 'darkslateblue': '#483d8b', 'darkslategray': '#2f4f4f', 'darkslategrey': '#2f4f4f', 'darkturquoise': '#00ced1', 'darkviolet': '#9400d3', 'deeppink': '#ff1493', 'deepskyblue': '#00bfff', 'dimgray': '#696969', 'dimgrey': '#696969', 'dodgerblue': '#1e90ff', 'firebrick': '#b22222', 'floralwhite': '#fffaf0', 'forestgreen': '#228b22', 'fuchsia': '#ff00ff', 'gainsboro': '#dcdcdc', 'ghostwhite': '#f8f8ff', 'gold': '#ffd700', 'goldenrod': '#daa520', 'gray': '#808080', 'green': '#008000', 'greenyellow': '#adff2f', 'grey': '#808080', 'honeydew': '#f0fff0', 'hotpink': '#ff69b4', 'indianred': '#cd5c5c', 'indigo': '#4b0082', 'ivory': '#fffff0', 'khaki': '#f0e68c', 'lavender': '#e6e6fa', 'lavenderblush': '#fff0f5', 'lawngreen': '#7cfc00', 'lemonchiffon': '#fffacd', 'lightblue': '#add8e6', 'lightcoral': '#f08080', 'lightcyan': '#e0ffff', 'lightgoldenrodyellow': '#fafad2', 'lightgray': '#d3d3d3', 'lightgreen': '#90ee90', 'lightgrey': '#d3d3d3', 'lightpink': '#ffb6c1', 'lightsalmon': '#ffa07a', 'lightseagreen': '#20b2aa', 'lightskyblue': '#87cefa', 'lightslategray': '#778899', 'lightslategrey': '#778899', 'lightsteelblue': '#b0c4de', 'lightyellow': '#ffffe0', 'lime': '#00ff00', 'limegreen': '#32cd32', 'linen': '#faf0e6', 'magenta': '#ff00ff', 'maroon': '#800000', 'mediumaquamarine': '#66cdaa', 'mediumblue': '#0000cd', 'mediumorchid': '#ba55d3', 'mediumpurple': '#9370db', 'mediumseagreen': '#3cb371', 'mediumslateblue': '#7b68ee', 'mediumspringgreen': '#00fa9a', 'mediumturquoise': '#48d1cc', 'mediumvioletred': '#c71585', 'midnightblue': '#191970', 'mintcream': '#f5fffa', 'mistyrose': '#ffe4e1', 'moccasin': '#ffe4b5', 'navajowhite': '#ffdead', 'navy': '#000080', 'oldlace': '#fdf5e6', 'olive': '#808000', 'olivedrab': '#6b8e23', 'orange': '#ffa500', 'orangered': '#ff4500', 'orchid': '#da70d6', 'palegoldenrod': '#eee8aa', 'palegreen': '#98fb98', 'paleturquoise': '#afeeee', 'palevioletred': '#db7093', 'papayawhip': '#ffefd5', 'peachpuff': '#ffdab9', 'peru': '#cd853f', 'pink': '#ffc0cb', 'plum': '#dda0dd', 'powderblue': '#b0e0e6', 'purple': '#800080', 'red': '#ff0000', 'rosybrown': '#bc8f8f', 'royalblue': '#4169e1', 'saddlebrown': '#8b4513', 'salmon': '#fa8072', 'sandybrown': '#f4a460', 'seagreen': '#2e8b57', 'seashell': '#fff5ee', 'sienna': '#a0522d', 'silver': '#c0c0c0', 'skyblue': '#87ceeb', 'slateblue': '#6a5acd', 'slategray': '#708090', 'slategrey': '#708090', 'snow': '#fffafa', 'springgreen': '#00ff7f', 'steelblue': '#4682b4', 'tan': '#d2b48c', 'teal': '#008080', 'thistle': '#d8bfd8', 'tomato': '#ff6347', 'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff', 'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32', } colorzero-release-2.0/colorzero/types.py000066400000000000000000000217721402377017200205570ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2016-2021 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause "Define the tuples used to represent various color systems." from collections import namedtuple, OrderedDict from .attr import Red, Green, Blue, Hue, Lightness, Saturation, Luma class RGB(tuple): "Named tuple representing red, green, and blue." # NOTE: This source mostly generated by namedtuple(..., verbose=True) # XXX When py3.6+ required, replace with derivative of typing.NamedTuple __slots__ = () _fields = ('r', 'g', 'b') def __new__(cls, r, g, b): return tuple.__new__(cls, (r, g, b)) def _replace(self, **kw): "Return a new RGB object replacing specified fields with new values" result = tuple.__new__(RGB, map(kw.pop, 'rgb', self)) if kw: raise ValueError( 'Got unexpected field names: {!r}'.format(list(kw))) return result def __repr__(self): "Return a nicely formatted representation string" return ( self.__class__.__name__ + '(r={self.r:g}, g={self.g:g}, b={self.b:g})'.format(self=self)) def _asdict(self): "Return a new OrderedDict which maps field names to their values." return OrderedDict(zip(self._fields, self)) def __getnewargs__(self): "Return self as a plain tuple. Used by copy and pickle." return tuple(self) @property def r(self): "Return the red value as a :class:`Red` instance" # pylint: disable=invalid-name return Red(self[0]) @property def red(self): "Return the red value as a :class:`Red` instance" return Red(self[0]) @property def g(self): "Return the green value as a :class:`Green` instance" # pylint: disable=invalid-name return Green(self[1]) @property def green(self): "Return the green value as a :class:`Green` instance" return Green(self[1]) @property def b(self): "Return the blue value as a :class:`Blue` instance" # pylint: disable=invalid-name return Blue(self[2]) @property def blue(self): "Return the blue value as a :class:`Blue` instance" return Blue(self[2]) class HLS(tuple): "Named tuple representing hue, lightness, and saturation." # NOTE: This source mostly generated by namedtuple(..., verbose=True) # XXX When py3.6+ required, replace with derivative of typing.NamedTuple __slots__ = () _fields = ('h', 'l', 's') def __new__(cls, h, l, s): return tuple.__new__(cls, (h, l, s)) def _replace(self, **kw): "Return a new HLS object replacing specified fields with new values" result = tuple.__new__(HLS, map(kw.pop, 'hls', self)) if kw: raise ValueError('Got unexpected field names: {!r}'.format(list(kw))) return result def __repr__(self): "Return a nicely formatted representation string" return ( self.__class__.__name__ + '(h={self.h:g}, l={self.l:g}, s={self.s:g})'.format(self=self)) def _asdict(self): "Return a new OrderedDict which maps field names to their values." return OrderedDict(zip(self._fields, self)) def __getnewargs__(self): "Return self as a plain tuple. Used by copy and pickle." return tuple(self) @property def h(self): "Return the hue value as a :class:`Hue` instance" # pylint: disable=invalid-name return Hue(self[0]) @property def hue(self): "Return the hue value as a :class:`Hue` instance" return Hue(self[0]) @property def l(self): "Return the lightness value as a :class:`Lightness` instance" # pylint: disable=invalid-name return Lightness(self[1]) @property def lightness(self): "Return the lightness value as a :class:`Lightness` instance" return Lightness(self[1]) @property def s(self): "Return the saturation value as a :class:`Saturation` instance" # pylint: disable=invalid-name return Saturation(self[2]) @property def saturation(self): "Return the saturation value as a :class:`Saturation` instance" return Saturation(self[2]) class HSV(tuple): 'Named tuple representing hue, saturation, and value ("brightness").' # XXX When py3.6+ required, replace with derivative of typing.NamedTuple __slots__ = () _fields = ('h', 's', 'v') def __new__(cls, h, s, v): return tuple.__new__(cls, (h, s, v)) def _replace(self, **kw): "Return a new HSV object replacing specified fields with new values" result = tuple.__new__(HSV, map(kw.pop, 'hsv', self)) if kw: raise ValueError( 'Got unexpected field names: {!r}'.format(list(kw))) return result def __repr__(self): "Return a nicely formatted representation string" return ( self.__class__.__name__ + '(h={self.h:g}, s={self.s:g}, v={self.v:g})'.format(self=self)) def _asdict(self): "Return a new OrderedDict which maps field names to their values." return OrderedDict(zip(self._fields, self)) def __getnewargs__(self): "Return self as a plain tuple. Used by copy and pickle." return tuple(self) @property def h(self): "Return the hue value as a :class:`Hue` instance" # pylint: disable=invalid-name return Hue(self[0]) @property def hue(self): "Return the hue value as a :class:`Hue` instance" return Hue(self[0]) @property def s(self): "Return the saturation value as a :class:`Saturation` instance" # pylint: disable=invalid-name return Saturation(self[1]) @property def saturation(self): "Return the saturation value as a :class:`Saturation` instance" return Saturation(self[1]) @property def v(self): "Return the brightness value" return self[2] @property def value(self): "Return the brightness value" return self[2] class YUV(tuple): "Named tuple representing luma and two chroma offsets" # XXX When py3.6+ required, replace with derivative of typing.NamedTuple __slots__ = () _fields = ('y', 'u', 'v') def __new__(cls, y, u, v): return tuple.__new__(cls, (y, u, v)) def _replace(self, **kw): "Return a new YUV object replacing specified fields with new values" result = tuple.__new__(YUV, map(kw.pop, 'yuv', self)) if kw: raise ValueError( 'Got unexpected field names: {!r}'.format(list(kw))) return result def __repr__(self): "Return a nicely formatted representation string" return ( self.__class__.__name__ + '(y={self.y:g}, u={self.u:g}, v={self.v:g})'.format(self=self)) def _asdict(self): "Return a new OrderedDict which maps field names to their values." return OrderedDict(zip(self._fields, self)) def __getnewargs__(self): "Return self as a plain tuple. Used by copy and pickle." return tuple(self) @property def y(self): "Return the luma value as a :class:`Luma` instance" # pylint: disable=invalid-name return Luma(self[0]) @property def luma(self): "Return the luma value as a :class:`Luma` instance" return Luma(self[0]) @property def u(self): "Return the first chroma offset" # pylint: disable=invalid-name return self[1] @property def v(self): "Return the second chroma offset" # pylint: disable=invalid-name return self[2] class CMY(namedtuple('CMY', ('c', 'm', 'y'))): "Named tuple representing cyan, magenta, and yellow." # XXX When py3.6+ required, replace with derivative of typing.NamedTuple @property def cyan(self): # pylint: disable=missing-docstring return self.c @property def magenta(self): # pylint: disable=missing-docstring return self.m @property def yellow(self): # pylint: disable=missing-docstring return self.y class CMYK(namedtuple('CMYK', ('c', 'm', 'y', 'k'))): "Named tuple representing cyan, magenta, yellow, and black." # XXX When py3.6+ required, replace with derivative of typing.NamedTuple @property def cyan(self): # pylint: disable=missing-docstring return self.c @property def magenta(self): # pylint: disable=missing-docstring return self.m @property def yellow(self): # pylint: disable=missing-docstring return self.y @property def black(self): # pylint: disable=missing-docstring return self.k YIQ = namedtuple('YIQ', ('y', 'i', 'q')) XYZ = namedtuple('XYZ', ('x', 'y', 'z')) Luv = namedtuple('Luv', ('l', 'u', 'v')) Lab = namedtuple('Lab', ('l', 'a', 'b')) colorzero-release-2.0/copyrights000077500000000000000000000352331402377017200171410ustar00rootroot00000000000000#!/usr/bin/python3 """ A script for updating the copyright headers on all files project-wide. It derives the authorship and copyright years information from the git history of the project; hence, this script must be run within a git repository. """ from __future__ import annotations import os import sys assert sys.version_info >= (3, 6), 'Script requires Python 3.6+' import tempfile import typing as t from argparse import ArgumentParser, Namespace from configparser import ConfigParser from operator import attrgetter from itertools import groupby from datetime import datetime from subprocess import Popen, PIPE, DEVNULL from pathlib import Path from fnmatch import fnmatch SPDX_PREFIX = 'SPDX-License-Identifier:' COPYRIGHT_PREFIX = 'Copyright (c)' def main(args: t.List[str] = None): if args is None: args = sys.argv[1:] config = get_config(args) writer = CopyWriter.from_config(config) for path, copyrights in get_copyrights(config.include, config.exclude): print(f'Re-writing {path}...') copyrights = sorted( copyrights, reverse=True, key=lambda c: (max(c.years), c.author)) with AtomicReplaceFile(path, encoding='utf-8') as target: with path.open('r') as source: for chunk in writer.transform(source, copyrights): target.write(chunk) def get_config(args: t.List[str]) -> Namespace: config = ConfigParser( defaults={ 'include': '**/*', 'exclude': '', 'license': 'LICENSE.txt', 'preamble': '', 'strip_preamble': 'false', 'spdx_prefix': SPDX_PREFIX, 'copy_prefix': COPYRIGHT_PREFIX, }, delimiters=('=',), default_section='copyrights:settings', empty_lines_in_values=False, interpolation=None, converters={'list': lambda s: s.strip().splitlines() }) config.read('setup.cfg') sect = config[config.default_section] parser = ArgumentParser(description=__doc__) parser.add_argument( '-i', '--include', action='append', metavar='GLOB', default=sect.getlist('include'), help="The set of patterns that a file must match to be included in " "the set of files to re-write. Can be specified multiple times to " "add several patterns. Default: %(default)r") parser.add_argument( '-e', '--exclude', action='append', metavar='GLOB', default=sect.getlist('exclude'), help="The set of patterns that a file must *not* match to be included " "in the set of files to re-write. Can be specified multiple times to " "add several patterns. Default: %(default)r") parser.add_argument( '-l', '--license', action='store', type=Path, metavar='PATH', default=sect['license'], help="The file containing the project's license text. If this file " "contains a SPDX-License-Identifier line (in addition to the license " "text itself), then matching license text found in source files will " "be replaced by the SPDX-License-Identifier line (appropriately " "commented). Default: %(default)s") parser.add_argument( '-p', '--preamble', action='append', metavar='STR', default=sect.getlist('preamble'), help="The line(s) of text to insert before the copyright attributions " "in source files. This is typically a brief description of the " "project. Can be specified multiple times to add several lines. " "Default: %(default)r") parser.add_argument( '-S', '--spdx-prefix', action='store', metavar='STR', default=sect['spdx_prefix'], help="The prefix on the line in the license file, and within comments " "of source files that identifies the appropriate license from the " "SPDX list. Default: %(default)r") parser.add_argument( '-C', '--copy-prefix', action='store', metavar='STR', default=sect['copy_prefix'], help="The prefix before copyright attributions in source files. " "Default: %(default)r") parser.add_argument( '--no-strip-preamble', action='store_false', dest='strip_preamble') parser.add_argument( '--strip-preamble', action='store_true', default=sect.getboolean('strip-preamble'), help="If enabled, any existing preamble matching that specified " "by --preamble will be removed. This can be used to change the " "preamble text in files by first specifying the old preamble with " "this option, then running a second time with the new preamble") ns = parser.parse_args(args) ns.include = set(ns.include) ns.exclude = set(ns.exclude) return ns class Copyright(t.NamedTuple): author: str email: str years: t.Set[int] def __str__(self): if len(self.years) > 1: years = f'{min(self.years)}-{max(self.years)}' else: years = f'{min(self.years)}' return f'{years} {self.author} <{self.email}>' def get_copyrights(include: t.Set[str], exclude: t.Set[str])\ -> t.Iterator[t.Tuple[Path, t.Container[Copyright]]]: sorted_blame = sorted( get_contributions(include, exclude), key=lambda c: (c.path, c.author, c.email) ) blame_by_file = { path: list(file_contributions) for path, file_contributions in groupby( sorted_blame, key=attrgetter('path') ) } for path, file_contributors in blame_by_file.items(): it = groupby(file_contributors, key=lambda c: (c.author, c.email)) copyrights = [ Copyright(author, email, {y.year for y in years}) for (author, email), years in it ] yield path, copyrights class Contribution(t.NamedTuple): author: str email: str year: int path: Path def get_contributions(include: t.Set[str], exclude: t.Set[str])\ -> t.Iterator[Contribution]: for path in get_source_paths(include, exclude): blame = Popen( ['git', 'blame', '--line-porcelain', 'HEAD', '--', str(path)], stdout=PIPE, stderr=PIPE, universal_newlines=True ) author = email = year = None if blame.stdout is not None: for line in blame.stdout: if line.startswith('author '): author = line.split(' ', 1)[1].rstrip() elif line.startswith('author-mail '): email = line.split(' ', 1)[1].rstrip() email = email.lstrip('<').rstrip('>') elif line.startswith('author-time '): # Forget the timezone; we only want the year anyway timestamp = int(line.split(' ', 1)[1].strip()) year = datetime.fromtimestamp(timestamp).year elif line.startswith('filename '): assert author is not None assert email is not None assert year is not None yield Contribution( author=author, email=email, year=year, path=path) author = email = year = None blame.wait() assert blame.returncode == 0 def get_source_paths(include: t.Set[str], exclude: t.Set[str])\ -> t.Iterator[Path]: ls_tree = Popen( ['git', 'ls-tree', '-r', '--name-only', 'HEAD'], stdout=PIPE, stderr=DEVNULL, universal_newlines=True) if not include: include = {'*'} if ls_tree.stdout is not None: for filename in ls_tree.stdout: filename = filename.strip() if any(fnmatch(filename, pattern) for pattern in exclude): continue if any(fnmatch(filename, pattern) for pattern in include): yield Path(filename) ls_tree.wait() assert ls_tree.returncode == 0 class License(t.NamedTuple): ident: t.Optional[str] text: t.List[str] def get_license(path: Path, *, spdx_prefix: str = SPDX_PREFIX) -> License: with open(path, 'r') as f: lines = f.read().splitlines() idents = [ line.rstrip() for line in lines if line.startswith(spdx_prefix) ] ident = None if len(idents) > 1: raise RuntimeError(f'More than one {spdx_prefix} line in {path}!') elif len(idents) == 1: ident = idents[0] body = [ line.rstrip() for line in lines if not line.startswith(spdx_prefix) ] while not body[0]: del body[0] while not body[-1]: del body[-1] return License(ident, body) class CopyWriter: """ Transformer for the copyright header in source files. The :meth:`transform` method can be called with a file-like object as the *source* and will yield chunks of replacement data to be written to the replacement. """ # The script's kinda dumb at this point - only handles straight-forward # line-based comments, not multi-line delimited styles like /*..*/ COMMENTS = { '': '#', '.c': '//', '.cpp': '//', '.js': '//', '.py': '#', '.rst': '..', '.sh': '#', '.sql': '--', } def __init__(self, license: Path=Path('LICENSE.txt'), preamble: t.List[str]=None, spdx_prefix: str=SPDX_PREFIX, copy_prefix: str=COPYRIGHT_PREFIX): if preamble is None: preamble = [] self.license = get_license(license, spdx_prefix=spdx_prefix) self.preamble = preamble self.spdx_prefix = spdx_prefix self.copy_prefix = copy_prefix @classmethod def from_config(cls, config: Namespace) -> CopyWriter: return cls( config.license, config.preamble, config.spdx_prefix, config.copy_prefix) def transform(self, source: t.TextIO, copyrights: t.List[Copyright], *, comment_prefix: str=None) -> t.Iterator[str]: if comment_prefix is None: comment_prefix = self.COMMENTS[Path(source.name).suffix] license_start = self.license.text[0] license_end = self.license.text[-1] state = 'header' empty = True for linenum, line in enumerate(source, start=1): if state == 'header': if linenum == 1 and line.startswith('#!'): yield line empty = False elif linenum < 3 and ( 'fileencoding=' in line or '-*- coding:' in line): yield line empty = False elif line.rstrip() == comment_prefix: pass # skip blank comment lines elif line.startswith(f'{comment_prefix} {self.spdx_prefix}'): pass # skip existing SPDX ident elif line.startswith(f'{comment_prefix} {self.copy_prefix}'): pass # skip existing copyright lines elif any(line.startswith(f'{comment_prefix} {pre_line}') for pre_line in self.preamble): pass # skip existing preamble elif line.startswith(f'{comment_prefix} {license_start}'): state = 'license' # skip existing license lines else: yield from self._generate_header( copyrights, comment_prefix, empty) state = 'blank' elif state == 'license': if line.startswith(f'{comment_prefix} {license_end}'): yield from self._generate_header( copyrights, comment_prefix, empty) state = 'blank' continue if state == 'blank': # Ensure there's a blank line between license and start of the # source body if line.strip(): yield '\n' yield line state = 'body' elif state == 'body': yield line def _generate_header(self, copyrights: t.Iterable[Copyright], comment_prefix: str, empty: bool) -> t.Iterator[str]: if not empty: yield comment_prefix + '\n' for line in self.preamble: yield f'{comment_prefix} {line}\n' if self.preamble: yield comment_prefix + '\n' for copyright in copyrights: yield f'{comment_prefix} {self.copy_prefix} {copyright!s}\n' yield comment_prefix + '\n' if self.license.ident: yield f'{comment_prefix} {self.license.ident}\n' else: for line in self.license.text: if line: yield f'{comment_prefix} {line}\n' else: yield comment_prefix + '\n' class AtomicReplaceFile: """ A context manager for atomically replacing a target file. Uses :class:`tempfile.NamedTemporaryFile` to construct a temporary file in the same directory as the target file. The associated file-like object is returned as the context manager's variable; you should write the content you wish to this object. When the context manager exits, if no exception has occurred, the temporary file will be renamed over the target file atomically (after copying permissions from the target file). If an exception occurs during the context manager's block, the temporary file will be deleted leaving the original target file unaffected and the exception will be re-raised. :param pathlib.Path path: The full path and filename of the target file. This is expected to be an absolute path. :param str encoding: If ``None`` (the default), the temporary file will be opened in binary mode. Otherwise, this specifies the encoding to use with text mode. """ def __init__(self, path: t.Union[str, Path], encoding: str = None): if isinstance(path, str): path = Path(path) self._path = path self._tempfile = tempfile.NamedTemporaryFile( mode='wb' if encoding is None else 'w', dir=str(self._path.parent), encoding=encoding, delete=False) self._withfile = None def __enter__(self): self._withfile = self._tempfile.__enter__() return self._withfile def __exit__(self, exc_type, exc_value, exc_tb): os.fchmod(self._withfile.file.fileno(), self._path.stat().st_mode) result = self._tempfile.__exit__(exc_type, exc_value, exc_tb) if exc_type is None: os.rename(self._withfile.name, str(self._path)) else: os.unlink(self._withfile.name) return result if __name__ == '__main__': main() colorzero-release-2.0/docs/000077500000000000000000000000001402377017200157425ustar00rootroot00000000000000colorzero-release-2.0/docs/Makefile000066400000000000000000000174741402377017200174170ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../build DOT_DIAGRAMS := $(wildcard images/*.dot) MSC_DIAGRAMS := $(wildcard images/*.mscgen) GPI_DIAGRAMS := $(wildcard images/*.gpi) SVG_IMAGES := $(wildcard images/*.svg) $(DOT_DIAGRAMS:%.dot=%.svg) $(MSC_DIAGRAMS:%.mscgen=%.svg) PNG_IMAGES := $(wildcard images/*.png) $(GPI_DIAGRAMS:%.gpi=%.png) $(SVG_IMAGES:%.svg=%.png) PDF_IMAGES := $(SVG_IMAGES:%.svg=%.pdf) $(GPI_DIAGRAMS:%.gpi=%.pdf) $(DOT_DIAGRAMS:%.dot=%.pdf) $(MSC_DIAGRAMS:%.mscgen=%.pdf) INKSCAPE_VER := $(shell inkscape --version | sed -ne '/^Inkscape/ s/^Inkscape \([0-9]\+\)\..*$$/\1/p') # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SVG_IMAGES) $(PNG_IMAGES) $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SVG_IMAGES) $(PNG_IMAGES) $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SVG_IMAGES) $(PNG_IMAGES) $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/colorzero.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/colorzero.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/colorzero" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/colorzero" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(PDF_IMAGES) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(PDF_IMAGES) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(PDF_IMAGES) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." %.svg: %.mscgen mscgen -T svg -o $@ $< %.svg: %.dot dot -T svg -o $@ $< %.png: %.gpi common.gp gnuplot -e "set term pngcairo size 640,480" $< > $@ ifeq ($(INKSCAPE_VER),0) %.png: %.svg inkscape --export-dpi 150 -e $@ $< %.pdf: %.svg inkscape -A $@ $< else %.png: %.svg inkscape --export-dpi 150 --export-type png -o $@ $< %.pdf: %.svg inkscape --export-type pdf -o $@ $< endif %.pdf: %.gpi common.gp gnuplot -e "set term pdfcairo size 10cm,7.5cm" $< > $@ %.pdf: %.mscgen mscgen -T eps -o - $< | ps2pdf -dEPSCrop - $@ .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext colorzero-release-2.0/docs/_static/000077500000000000000000000000001402377017200173705ustar00rootroot00000000000000colorzero-release-2.0/docs/_static/style_override.css000066400000000000000000000005161402377017200231430ustar00rootroot00000000000000/* override table width restrictions */ .wy-table-responsive table td, .wy-table-responsive table th { /* !important prevents the common CSS stylesheets from overriding this as on RTD they are loaded after this stylesheet */ white-space: normal !important; } .wy-table-responsive { overflow: visible !important; } colorzero-release-2.0/docs/api_color.rst000066400000000000000000000110011402377017200204340ustar00rootroot00000000000000.. The colorzero color library .. .. Copyright (c) 2016-2019 Dave Jones .. .. SPDX-License-Identifier: BSD-3-Clause .. _api_color: === API === .. currentmodule:: colorzero The colorzero library includes a comprehensive :class:`Color` class which is capable of converting between numerous color representations and calculating color differences. Various ancillary classes can be used to manipulate aspects of a color. Color Class =========== This the primary class in the package, and often the only class you'll need or want to interact with. It has an extremely flexible constructor, along with numerous explicit constructors, and attributes for conversion to other color systems. .. autoclass:: Color .. _format: Format Strings ============== Instances of :class:`Color` can be used in format strings to output various representations of a color, including HTML sequences and ANSI escape sequences to color terminal output. Format specifications can be used to modify the output to support different terminal types. For example: .. code-block:: pycon >>> red = Color('red') >>> green = Color('green') >>> blue = Color('#47b') >>> print("{red:html}".format(red=red)) #ff0000 >>> print(repr("{red}Red{red:0} Alert!".format(red=red))) '\\x1b[1;31mRed\\x1b[0m Alert!' >>> print(repr("The grass is {green:16m}greener{green:0}.".format( ... green=green))) 'The grass is \\x1b[38;2;0;128;0mgreener\\x1b[0m.' >>> print(repr("{blue:b16m}Blue skies{blue:0}".format(blue=blue))) '\\x1b[48;2;68;119;187mBlue skies\\x1b[0m' The format specification is one of: * "html" - the color will be output as the common 7-character HTML represention of #RRGGBB where RR, GG, and BB are the red, green and blue components expressed as a single hexidecimal byte * "css" or "cssrgb" - the color will be output in CSS' functional notation rgb(*r*, *g*, *b*) where *r*, *g*, and *b* are decimal representations of the red, green, and blue components in the range 0 to 255 * "csshsl" - the color will be output in CSS' function notation hue(*h*\deg, *s*\%, *l*\%) where *h*, *s*, and *l* are the hue (expressed in degrees), saturation, and lightness (expressed as percentages) * One of the ANSI format specifications which consist of an optional foreground / background specifier (the letters "f" or "b") followed by an optional terminal type identifer, which is one of: - "8" - the default, indicating only the original 8 DOS colors are supported (technically, 16 foreground colors are supported via use of the "bold" style for "intense" colors) - "256" - indicates the terminal supports 256 colors via `8-bit color ANSI codes`_ - "16m" - indicating the terminal supports ~16 million colors via `24-bit color ANSI codes`_ Alternately, "0" can be specified indicating that the style should be reset. If specified with the optional foreground / background specifier, "0" resets only the foreground / background color. If specified alone it resets all styles. More formally: .. code-block:: bnf ::= "" | "f" | "b" ::= "" | "0" | "8" | "256" | "16m" ::= ::= "html" ::= "css" ("rgb" | "hsl")? ::= | | .. versionadded:: 1.1 The ability to output ANSI codes via format strings, and the customization of :func:`repr` output. .. versionadded:: 1.2 The ability to output HTML and CSS representations via format strings .. _8-bit color ANSI codes: https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit .. _24-bit color ANSI codes: https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit Manipulation Classes ==================== These manipulation classes are used in conjunction with the standard arithmetic addition, subtraction, and multiplication operators to calculate new :class:`Color` instances. .. autoclass:: Red .. autoclass:: Green .. autoclass:: Blue .. autoclass:: Hue .. autoclass:: Saturation .. autoclass:: Lightness .. autoclass:: Luma Difference Functions ==================== .. autofunction:: euclid .. autofunction:: cie1976 .. autofunction:: cie1994g .. autofunction:: cie1994t .. autofunction:: ciede2000 Easing Functions ================ These functions can be used with the :meth:`Color.gradient` method to control the progression of the fade between the two colors. .. autofunction:: linear .. autofunction:: ease_in .. autofunction:: ease_out .. autofunction:: ease_in_out colorzero-release-2.0/docs/changelog.rst000066400000000000000000000017241402377017200204270ustar00rootroot00000000000000.. The colorzero color library .. .. Copyright (c) 2016-2018 Dave Jones .. .. SPDX-License-Identifier: BSD-3-Clause ========== Change log ========== .. currentmodule:: colorzero Release 2.0 (2021-03-15) ======================== * Dropped Python 2.x support. Current Python support level is 3.5 and above. * Added html and css format specifications to the :class:`Color` class' string-formatting capabilities. Release 1.1 (2018-05-15) ======================== * Added ability to generate ANSI codes with :ref:`format`. * Added :meth:`Color.gradient` method. * Exposed the various difference functions in the API (:func:`euclid`, :func:`cie1976`, etc). * Various doc fixes and enhancements. Release 1.0 (2018-03-10) ======================== 1.0 is the first release after breaking the library out of the `picamera`_ project. As this is a 1.x release, API stability will be maintained. .. _picamera: https://github.com/waveform80/picamera colorzero-release-2.0/docs/conf.py000066400000000000000000000106601402377017200172440ustar00rootroot00000000000000#!/usr/bin/env python3 # vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2016-2021 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause import sys import os import pkginfo from datetime import datetime on_rtd = os.environ.get('READTHEDOCS', None) == 'True' info = pkginfo.Installed('colorzero') if info.version is None: # Probably a git check-out; treat it as a development repo and ask git # for the path to the root of the check-out import subprocess as sp root = sp.run(['git', 'rev-parse', '--show-cdup'], stdout=sp.PIPE, stderr=sp.STDOUT, text=True, check=True).stdout.strip() info = pkginfo.Develop(root) if info.version is None: raise RuntimeError('Failed to load distro info') # -- General configuration ------------------------------------------------ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] if on_rtd: needs_sphinx = '1.4.0' extensions.append('sphinx.ext.imgmath') imgmath_image_format = 'svg' tags.add('rtd') else: extensions.append('sphinx.ext.mathjax') mathjax_path = '/usr/share/javascript/mathjax/MathJax.js?config=TeX-AMS_HTML' templates_path = ['_templates'] source_suffix = '.rst' #source_encoding = 'utf-8-sig' master_doc = 'index' project = info.name copyright = '2016-{now:%Y} {info.author}'.format(now=datetime.now(), info=info) version = info.version #release = None #language = None #today_fmt = '%B %d, %Y' exclude_patterns = ['_build'] highlight_language = 'python3' #default_role = None #add_function_parentheses = True #add_module_names = True #show_authors = False pygments_style = 'sphinx' #modindex_common_prefix = [] #keep_warnings = False # -- Autodoc configuration ------------------------------------------------ autodoc_member_order = 'groupwise' autodoc_default_flags = ['members'] # -- Intersphinx configuration -------------------------------------------- intersphinx_mapping = { 'python': ('https://docs.python.org/3.5', None), } intersphinx_cache_limit = 7 # -- Options for HTML output ---------------------------------------------- html_theme = 'sphinx_rtd_theme' pygments_style = 'default' html_title = '{info.name} {info.version} Documentation'.format(info=info) #html_theme_path = [] #html_short_title = None #html_logo = None #html_favicon = None html_static_path = ['_static'] #html_extra_path = [] #html_last_updated_fmt = '%b %d, %Y' #html_use_smartypants = True #html_additional_pages = {} #html_domain_indices = True #html_use_index = True #html_split_index = False #html_show_sourcelink = True #html_show_sphinx = True #html_show_copyright = True #html_use_opensearch = '' #html_file_suffix = None htmlhelp_basename = '{info.name}doc'.format(info=info) # Hack to make wide tables work properly in RTD # See https://github.com/snide/sphinx_rtd_theme/issues/117 for details def setup(app): app.add_stylesheet('style_override.css') # -- Options for LaTeX output --------------------------------------------- latex_engine = 'xelatex' latex_elements = { 'papersize': 'a4paper', 'pointsize': '10pt', 'preamble': r'\def\thempfootnote{\arabic{mpfootnote}}', # workaround sphinx issue #2530 } latex_documents = [ ( 'index', # source start file project + '.tex', # target filename html_title, # title info.author, # author 'manual', # documentclass True, # documents ref'd from toctree only ), ] #latex_logo = None #latex_use_parts = False latex_show_pagerefs = True latex_show_urls = 'footnote' #latex_appendices = [] #latex_domain_indices = True # -- Options for epub output ---------------------------------------------- epub_basename = project #epub_theme = 'epub' #epub_title = html_title epub_author = info.author epub_identifier = 'https://{info.name}.readthedocs.io/'.format(info=info) #epub_tocdepth = 3 epub_show_urls = 'no' #epub_use_index = True # -- Options for manual page output --------------------------------------- man_pages = [] man_show_urls = True # -- Options for Texinfo output ------------------------------------------- texinfo_documents = [] #texinfo_appendices = [] #texinfo_domain_indices = True #texinfo_show_urls = 'footnote' #texinfo_no_detailmenu = False # -- Options for linkcheck builder ---------------------------------------- linkcheck_retries = 3 linkcheck_workers = 20 linkcheck_anchors = True colorzero-release-2.0/docs/development.rst000066400000000000000000000057631402377017200210310ustar00rootroot00000000000000.. The colorzero color library .. .. Copyright (c) 2016-2021 Dave Jones .. .. SPDX-License-Identifier: BSD-3-Clause =========== Development =========== .. currentmodule:: colorzero The main GitHub repository for the project can be found at: https://github.com/waveform80/colorzero Anyone is more than welcome to open tickets to discuss bugs, new features, or just to ask usage questions (I find this useful for gauging what questions ought to feature in the FAQ, for example). Even if you don’t feel up to hacking on the code, I’d love to hear suggestions from people of what you’d like the API to look like (even if the code itself isn’t particularly pythonic, the interface should be)! .. _dev_install: Development installation ======================== If you wish to develop colorzero itself, it is easiest to obtain the source by cloning the GitHub repository and then use the "develop" target of the Makefile which will install the package as a link to the cloned repository allowing in-place development (it also builds a tags file for use with vim/emacs with Exuberant’s ctags utility). The following example demonstrates this method within a virtual Python environment: .. code-block:: console $ sudo apt install build-essential git \ exuberant-ctags virtualenvwrapper python-virtualenv python3-virtualenv $ cd $ mkvirtualenv -p /usr/bin/python3 colorzero $ workon colorzero (colorzero) $ git clone https://github.com/waveform80/colorzero.git (colorzero) $ cd colorzero (colorzero) $ make develop To pull the latest changes from git into your clone and update your installation: .. code-block:: console $ workon colorzero (colorzero) $ cd ~/colorzero (colorzero) $ git pull (colorzero) $ make develop To remove your installation, destroy the sandbox and the clone: .. code-block:: console (colorzero) $ deactivate $ rmvirtualenv colorzero $ rm -fr ~/colorzero Building the docs ================= If you wish to build the docs, you'll need a few more dependencies. Inkscape is used for conversion of SVGs to other formats, Graphviz is used for rendering certain charts, and TeX Live is required for building PDF output. The following command should install all required dependencies: .. code-block:: console $ sudo apt install texlive-latex-recommended texlive-latex-extra \ texlive-fonts-recommended texlive-xetex graphviz inkscape xindy Once these are installed, you can use the "doc" target to build the documentation: .. code-block:: console $ workon colorzero (colorzero) $ cd ~/colorzero (colorzero) $ make doc The HTML output is written to :file:`build/html` while the PDF output goes to :file:`build/latex`. Test suite ========== If you wish to run the colorzero test suite, follow the instructions in :ref:`dev_install` above and then make the "test" target within the sandbox: .. code-block:: console $ workon colorzero (colorzero) $ cd ~/colorzero (colorzero) $ make test colorzero-release-2.0/docs/index.rst000066400000000000000000000006521402377017200176060ustar00rootroot00000000000000.. The colorzero color library .. .. Copyright (c) 2016-2018 Dave Jones .. .. SPDX-License-Identifier: BSD-3-Clause .. include:: ../README.rst Table of Contents ================= .. toctree:: :maxdepth: 1 :numbered: install quickstart api_color development changelog license Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` colorzero-release-2.0/docs/install.rst000066400000000000000000000031601402377017200201420ustar00rootroot00000000000000.. The colorzero color library .. .. Copyright (c) 2016-2021 Dave Jones .. .. SPDX-License-Identifier: BSD-3-Clause ============ Installation ============ .. currentmodule:: colorzero Raspbian installation ===================== On `Raspbian`_, it is best to obtain colorzero via the ``apt`` utility: .. code-block:: console $ sudo apt update $ sudo apt install python3-colorzero The usual apt upgrade method can be used to keep your installation up to date: .. code-block:: console $ sudo apt update $ sudo apt upgrade To remove your installation: .. code-block:: console $ sudo apt remove python3-colorzero Ubuntu installation =================== If you are using `Ubuntu`_, it is probably easiest to obtain colorzero from the author's PPA: .. code-block:: console $ sudo add-apt-repository ppa://waveform/ppa $ sudo apt update $ sudo apt install python3-colorzero The usual apt upgrade method can be used to keep your installation up to date: .. code-block:: console $ sudo apt update $ sudo apt upgrade To remove your installation: .. code-block:: console $ sudo apt remove python3-colorzero Other platforms =============== On other platforms, it is probably easiest to obtain colorzero via the ``pip`` utility: .. code-block:: console $ sudo pip3 install colorzero To upgrade your installation: .. code-block:: console $ sudo pip3 install -U colorzero To remove your installation: .. code-block:: console $ sudo pip3 remove colorzero .. _Raspbian: https://www.raspberrypi.org/downloads/raspbian/ .. _Ubuntu: https://ubuntu.com/ colorzero-release-2.0/docs/license.rst000066400000000000000000000030051402377017200201140ustar00rootroot00000000000000======= License ======= Copyright 2016-2021 `Dave Jones`_ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. .. _Dave Jones: mailto:dave@waveform.org.uk colorzero-release-2.0/docs/quickstart.rst000066400000000000000000000154141402377017200206730ustar00rootroot00000000000000.. The colorzero color library .. .. Copyright (c) 2016-2021 Dave Jones .. .. SPDX-License-Identifier: BSD-3-Clause =============== Getting started =============== .. currentmodule:: colorzero The :class:`Color` class is the main interface provided by colorzero. It can be constructed in a large variety of ways including with red, green, and blue components, "well known" color names (taken from CSS 3's `extended color keywords`_), HTML color specifications, and more. A selection of valid constructors is shown below: .. code-block:: pycon >>> from colorzero import * >>> Color('red') >>> Color(1.0, 0.0, 0.0) >>> Color(255, 0, 0) >>> Color('#ff0000') >>> Color('#f00') Internally, colorzero always represents colors as red, green, and blue values between 0.0 and 1.0. :class:`Color` objects are tuple descendents. Crucially, this means they are *immutable*. Attempting to change the red, green, or blue attributes will fail: .. code-block:: pycon >>> c = Color('red') >>> c.red Red(1.0) >>> c.red = 0.5 Traceback (most recent call last): File "", line 1, in AttributeError: can't set attribute In order to manipulate a color, colorzero provides a simple series of classes which represent attributes of a color: :class:`Red`, :class:`Green`, :class:`Blue`, :class:`Hue`, :class:`Lightness`, :class:`Saturation` and so on. You can use these classes in combination with Python's usual mathematical operators (addition, subtraction, multiplication, etc.) to manipulate a color. For example, continuing the example from above: .. code-block:: pycon >>> c + Green(0.1) >>> c = c + Green(0.5) >>> c >>> c.lightness Lightness(0.5) >>> c = c * Lightness(0.5) >>> c Numerous attributes are provided to enable conversion of the RGB representation to other systems: .. code-block:: pycon >>> c.rgb RGB(r=0.5, g=0.25, b=0.0) >>> c.rgb_bytes RGB(r=128, g=64, b=0) >>> c.rgb565 31200 >>> c.hls HLS(h=0.08333333333333333, l=0.25, s=1.0) >>> c.xyz XYZ(x=0.10647471144683732, y=0.0819048964489466, z=0.010202272707313633) >>> c.lab Lab(l=34.376494620040376, a=23.890819210881016, b=44.69197916172735) Equivalent constructors exist for all these systems: .. code-block:: pycon >>> Color.from_rgb(0.5, 0.25, 0.0) >>> Color.from_rgb_bytes(128, 64, 0) >>> Color.from_rgb565(31200) >>> Color.from_hls(*c.hls) >>> Color.from_xyz(*c.xyz) >>> Color.from_lab(*c.lab) Note that some conversions lose a certain amount of precision. The :func:`repr` output of :class:`Color` is relatively verbose by default, but this can be customized via the :class:`Color.repr_style` class attribute: .. code-block:: pycon >>> c = Color('red') >>> c >>> Color.repr_style = 'html' >>> c Color('#ff0000') >>> Color.repr_style = 'rgb' >>> c Color(1, 0, 0) If you have a terminal capable of color output (usually this means an actual terminal, not those integrated into applications like IDLE, Thonny, etc.), you can also preview colors with this facility (the output below shows the ANSI codes produced, but the documentation system won't reproduce the colored output): .. code-block:: pycon >>> Color.repr_style = 'term256' >>> c >>> repr(c) '' >>> Color.repr_style = 'term16m' >>> c >>> repr(c) '' These ANSI codes can also be generated by using colors with :meth:`str.format`. For example: .. code-block:: pycon >>> '{c:16m}Red{c:0} Alert!'.format(c=Color('red')) '\x1b[38;2;255;0;0mRed\x1b[0m Alert!' See :ref:`format` for more information. A method (:meth:`~Color.gradient`) is provided to generate gradients which fade from one color to another. The result is a generator, which must be iterated over if you want all the results: .. code-block:: pycon >>> Color.repr_style = 'term16m' >>> for c in Color('red').gradient(Color('green')): ... print(repr(c)) ... In a color-capable terminal, the "###" above will appear to fade between the two specified colors. Methods are also provided to compare colors for similarity. The simplest algorithm (and the default) is "euclid" which calculates the difference as the distance between them by treating the r, g, b components as coordinates in a 3-dimensional space. The same color will have a distance of 0.0, whilst the largest possible difference is :math:`\sqrt{3}` (:math:`\approx 1.732`): .. code-block:: pycon >>> c1 = Color('red') >>> c2 = Color('green') >>> c3 = c1 * Lightness(0.9) >>> c1.difference(c2, 'euclid') 1.1189122525867927 >>> c1.difference(c2) 1.1189122525867927 >>> c1.difference(c3) 0.09999999999999998 Various `Delta-E`_ algorithms (CIE1976, CIE1994, and CIEDE2000) are also provided. In these systems, 2.3 is considered a "just noticeable difference": .. code-block:: pycon >>> c1.difference(c2, 'cie1976') 133.10729836196307 >>> c1.difference(c3, 'cie1976') 9.60280542204272 >>> c1.difference(c2, 'cie1994g') 50.97596644678241 >>> c1.difference(c3, 'cie1994g') 5.484832836355026 >>> c1.difference(c2, 'ciede2000') 72.18229138962074 >>> c1.difference(c3, 'ciede2000') 5.490813507834904 These algorithms are also available as straight-forward functions: .. code-block:: pycon >>> cie1976(c1, c2) 133.10729836196307 >>> ciede2000(c1, c3) 5.490813507834904 .. _extended color keywords: https://www.w3.org/TR/css3-color/#svg-color .. _Delta-E: https://en.wikipedia.org/wiki/Color_difference colorzero-release-2.0/rtd_requirements.txt000066400000000000000000000000101402377017200211360ustar00rootroot00000000000000pkginfo colorzero-release-2.0/setup.cfg000066400000000000000000000027071402377017200166410ustar00rootroot00000000000000[metadata] name = colorzero version = 2.0 description = Yet another Python color library long_description = file:README.rst author = Dave Jones author_email = dave@waveform.org.uk license = BSD-3-Clause project_urls = Documentation = https://colorzero.readthedocs.io/ Source Code = https://github.com/waveform80/colorzero/ Issue Tracker = https://github.com/waveform80/colorzero/issues keywords = color classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Education Intended Audience :: Developers Topic :: Education License :: OSI Approved :: BSD License Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: PyPy [options] packages = find: install_requires = setuptools [options.extras_require] test = pytest pytest-cov doc = pkginfo sphinx sphinx-rtd-theme [tool:pytest] addopts = -rsx --cov --tb=short testpaths = tests [coverage:run] source = colorzero branch = true [coverage:report] show_missing = true exclude_lines = raise NotImplementedError assert False [copyrights:settings] include= **/*.py **/*.rst exclude= docs/license.rst license=LICENSE.txt preamble= The colorzero color library strip-preamble=no colorzero-release-2.0/setup.py000066400000000000000000000000461402377017200165240ustar00rootroot00000000000000from setuptools import setup setup() colorzero-release-2.0/tests/000077500000000000000000000000001402377017200161545ustar00rootroot00000000000000colorzero-release-2.0/tests/compat.py000066400000000000000000000013431402377017200200120ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2018 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause import cmath # Back-ported from python 3.5; see # github.com/PythonCHB/close_pep/blob/master/is_close.py for original # implementation def isclose(a, b, rel_tol=1e-9, abs_tol=0.0): if rel_tol < 0.0 or abs_tol < 0.0: raise ValueError('error tolerances must be non-negative') if a == b: # fast-path for exact equality return True if cmath.isinf(a) or cmath.isinf(b): return False diff = abs(b - a) return ( (diff <= abs(rel_tol * b)) or (diff <= abs(rel_tol * a)) or (diff <= abs_tol) ) colorzero-release-2.0/tests/test_attr.py000066400000000000000000000030571402377017200205440ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2018 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause "Tests for the colorzero.attr module" from __future__ import ( unicode_literals, print_function, division, absolute_import, ) from math import pi # pylint: disable=import-error,missing-docstring import pytest from colorzero import * def test_hue_init(): assert Hue(0.0) == 0.0 assert Hue(1.0) == 0.0 assert Hue(deg=0) == 0.0 assert Hue(deg=360) == 0.0 assert Hue(deg=720) == 0.0 assert Hue(deg=-180) == 0.5 assert Hue(deg=180) == 0.5 assert Hue(rad=0) == 0.0 assert Hue(rad=pi) == 0.5 assert Hue(rad=2 * pi) == 0.0 with pytest.raises(ValueError): Hue() def test_red_repr(): assert repr(Red(0.5)) == 'Red(0.5)' def test_green_repr(): assert repr(Green(1.0)) == 'Green(1)' def test_blue_repr(): assert repr(Blue(0.75)) == 'Blue(0.75)' def test_hue_attr(): assert Hue(0).deg == 0 assert Hue(0.5).deg == 180 assert Hue(1 / 3).deg == 120 assert Hue(0).rad == 0 assert Hue(0.5).rad == pi assert Hue(1 / 3).rad == (2 / 3) * pi def test_hue_repr(): assert repr(Hue(0)) == 'Hue(deg=0)' assert repr(Hue(0.5)) == 'Hue(deg=180)' assert repr(Hue(1/3)) == 'Hue(deg=120)' def test_sat_repr(): assert repr(Saturation(1.0)) == 'Saturation(1)' def test_light_repr(): assert repr(Lightness(0.25)) == 'Lightness(0.25)' def test_luma_repr(): assert repr(Luma(0.0)) == 'Luma(0)' colorzero-release-2.0/tests/test_color.py000066400000000000000000000355451402377017200207170ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2018-2019 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause "Tests for the colorzero.color module" from __future__ import ( unicode_literals, print_function, division, absolute_import, ) try: from math import sqrt, isclose except ImportError: from math import sqrt from compat import isclose # pylint: disable=wrong-import-order,import-error,missing-docstring import pytest from colorzero import ( Color, RGB, YUV, YIQ, HLS, HSV, CMY, CMYK, XYZ, Lab, Luv, Red, Green, Blue, Hue, Lightness, Saturation, Luma ) def verify_color(color1, color2, abs_tol=1e-7): for elem1, elem2 in zip(color1, color2): assert isclose(elem1, elem2, abs_tol=abs_tol) def test_color_new(): verify_color(Color(0, 0, 0), (0.0, 0.0, 0.0)) verify_color(Color(1, 1, 1), (1.0, 1.0, 1.0)) verify_color(Color(255, 255, 255), (1.0, 1.0, 1.0)) verify_color(Color(r=1, g=0, b=0), (1.0, 0.0, 0.0)) verify_color(Color(r=255, g=255, b=255), (1.0, 1.0, 1.0)) verify_color(Color(red=1, green=0, blue=0.0), (1.0, 0.0, 0.0)) verify_color(Color(red=255, green=255, blue=255), (1.0, 1.0, 1.0)) verify_color(Color(y=1, u=0, v=0), (1.0, 1.0, 1.0)) verify_color(Color(y=16, u=128, v=128), (0.0, 0.0, 0.0)) verify_color(Color('red'), (1.0, 0.0, 0.0)) verify_color(Color(b'red'), (1.0, 0.0, 0.0)) verify_color(Color((1, 0, 0)), (1.0, 0.0, 0.0)) verify_color(Color(RGB(1, 0, 0)), (1.0, 0.0, 0.0)) verify_color(Color(0), (0.0, 0.0, 0.0)) verify_color(Color(0xff), (1.0, 0.0, 0.0)) verify_color(Color(0xff0000), (0.0, 0.0, 1.0)) verify_color(Color(l=100, a=0, b=0), (1.0, 1.0, 1.0)) verify_color(Color(l=100, u=0, v=0), (1.0, 1.0, 1.0)) verify_color(Color(h=0, l=1, s=0), (1.0, 1.0, 1.0)) verify_color(Color(hue=0, lightness=1, saturation=0), (1.0, 1.0, 1.0)) verify_color(Color(h=0, s=0, v=1), (1.0, 1.0, 1.0)) verify_color(Color(hue=0, saturation=0, value=1), (1.0, 1.0, 1.0)) verify_color(Color(c=0, m=1, y=1), (1.0, 0.0, 0.0)) verify_color(Color(cyan=0, magenta=1, yellow=1), (1.0, 0.0, 0.0)) verify_color(Color(c=0, m=1, y=1, k=0), (1.0, 0.0, 0.0)) verify_color(Color(cyan=0, magenta=1, yellow=1, black=0), (1.0, 0.0, 0.0)) with pytest.raises(ValueError): Color() with pytest.raises(ValueError): Color(foo=1, bar=2) with pytest.raises(ValueError): Color((1, 0, 0, 0)) with pytest.raises(ValueError): Color(0.1) def test_color_from_rgb(): verify_color(Color.from_rgb(0, 0, 0), (0.0, 0.0, 0.0)) verify_color(Color.from_rgb(1, 1, 1), (1.0, 1.0, 1.0)) verify_color(Color.from_rgb(2, 1, 1), (1.0, 1.0, 1.0)) verify_color(Color.from_rgb(1, -1, 1), (1.0, 0.0, 1.0)) verify_color(Color.from_rgb(r=1, g=0, b=0), (1.0, 0.0, 0.0)) def test_color_from_rgb565(): verify_color(Color.from_rgb565(0x0000), (0.0, 0.0, 0.0)) verify_color(Color.from_rgb565(0xffff).rgb, (1.0, 1.0, 1.0)) verify_color(Color.from_rgb565(1).rgb_bytes, (0, 0, 8)) verify_color(Color.from_rgb565(1 << 5).rgb_bytes, (0, 4, 0)) verify_color(Color.from_rgb565(1 << 11).rgb_bytes, (8, 0, 0)) def test_color_from_rgb_bytes(): verify_color(Color.from_rgb_bytes(1, 1, 1).rgb_bytes, (1, 1, 1)) verify_color(Color.from_rgb_bytes(255, 255, 255), (1.0, 1.0, 1.0)) verify_color(Color.from_rgb_bytes(r=255, g=255, b=255), (1.0, 1.0, 1.0)) def test_color_from_yuv(): verify_color(Color.from_yuv(0, 0, 0), (0.0, 0.0, 0.0)) verify_color(Color.from_yuv(1, 0, 0), (1.0, 1.0, 1.0)) verify_color(Color.from_yuv(-1, 0, 0), (0.0, 0.0, 0.0)) verify_color(Color.from_yuv(2, 0, 0), (1.0, 1.0, 1.0)) verify_color(Color.from_yuv(y=0.299, u=-0.14713769751693, v=0.615), (1.0, 0.0, 0.0)) def test_color_from_yuv_bytes(): verify_color(Color.from_yuv_bytes(16, 128, 128), (0.0, 0.0, 0.0)) verify_color(Color.from_yuv_bytes(235, 128, 128), (1.0, 1.0, 1.0)) verify_color(Color.from_yuv_bytes(-255, 128, 128), (0.0, 0.0, 0.0)) verify_color(Color.from_yuv_bytes(512, 128, 128), (1.0, 1.0, 1.0)) verify_color(Color.from_yuv_bytes(81, 90, 240), (1.0, 0.0, 0.0)) def test_color_from_yiq(): verify_color(Color.from_yiq(0, 0, 0), (0.0, 0.0, 0.0)) verify_color(Color.from_yiq(1, 0, 0), (1.0, 1.0, 1.0)) verify_color(Color.from_yiq(2, 0, 0), (1.0, 1.0, 1.0)) verify_color(Color.from_yiq(-1, 0, 0), (0.0, 0.0, 0.0)) verify_color(Color.from_yiq(0.3, 0.599, 0.213), (1.0, 0.0, 0.0)) def test_color_from_xyz(): verify_color(Color.from_xyz(0, 0, 0), (0.0, 0.0, 0.0)) verify_color(Color.from_xyz(0.95047, 1, 1.08883), (1.0, 1.0, 1.0)) verify_color(Color.from_xyz(0.4124564, 0.2126729, 0.0193339), (1.0, 0.0, 0.0), abs_tol=1e-5) def test_color_from_lab(): verify_color(Color.from_lab(0, 0, 0), (0.0, 0.0, 0.0)) verify_color(Color.from_lab(100, 0, 0), (1.0, 1.0, 1.0)) verify_color(Color.from_lab(53.24, 80.1, 67.2), (1.0, 0.0, 0.0), abs_tol=1e-4) def test_color_from_luv(): verify_color(Color.from_luv(0, 0, 0), (0.0, 0.0, 0.0)) verify_color(Color.from_luv(100, 0, 0), (1.0, 1.0, 1.0)) verify_color(Color.from_luv(53.24079, 175.01503, 37.75643), (1.0, 0.0, 0.0), abs_tol=1e-5) def test_color_from_hls(): verify_color(Color.from_hls(0, 0, 0), (0.0, 0.0, 0.0)) verify_color(Color.from_hls(0, -1, 0), (0.0, 0.0, 0.0)) verify_color(Color.from_hls(0, 1, 0), (1.0, 1.0, 1.0)) verify_color(Color.from_hls(0, 2, 0), (1.0, 1.0, 1.0)) verify_color(Color.from_hls(0, 0.5, 1), (1.0, 0.0, 0.0)) def test_color_from_hsv(): verify_color(Color.from_hsv(0, 0, 0), (0.0, 0.0, 0.0)) verify_color(Color.from_hsv(0, 0, 1), (1.0, 1.0, 1.0)) verify_color(Color.from_hsv(0, 0, 2), (1.0, 1.0, 1.0)) verify_color(Color.from_hsv(0, 1, 1), (1.0, 0.0, 0.0)) def test_color_from_cmy(): verify_color(Color.from_cmy(1, 1, 1), (0.0, 0.0, 0.0)) verify_color(Color.from_cmy(2, 1, 1), (0.0, 0.0, 0.0)) verify_color(Color.from_cmy(0, 0, 0), (1.0, 1.0, 1.0)) verify_color(Color.from_cmy(-1, 0, 0), (1.0, 1.0, 1.0)) verify_color(Color.from_cmy(0, 1, 1), (1.0, 0.0, 0.0)) def test_color_from_cmyk(): verify_color(Color.from_cmyk(0, 0, 0, 1), (0.0, 0.0, 0.0)) verify_color(Color.from_cmyk(0, 0, 0, 0), (1.0, 1.0, 1.0)) verify_color(Color.from_cmyk(0, 1, 1, 0), (1.0, 0.0, 0.0)) def test_color_add(): verify_color(Color('red') + Color('blue'), Color('magenta')) verify_color(Color('red') + Color('white'), Color('white')) verify_color(Color('red') + Red(1), Color('red')) verify_color(Color('red') + Green(1), Color('yellow')) verify_color(Color('red') + Blue(1), Color('magenta')) verify_color(Color('red') + Hue(0), Color('red')) verify_color(Color('red') + Lightness(0.5), Color('white')) verify_color(Color('red') + Saturation(1), Color('red')) verify_color(Color('red') + Luma(1), Color('white')) verify_color(Green(1) + Color('red'), Color('yellow')) # pylint: disable=expression-not-assigned with pytest.raises(TypeError): Color('red') + 1 with pytest.raises(TypeError): 1 + Color('red') def test_color_sub(): verify_color(Color('magenta') - Color('blue'), Color('red')) verify_color(Color('magenta') - Color('white'), Color('black')) verify_color(Color('magenta') - Red(1), Color('blue')) verify_color(Color('magenta') - Green(1), Color('magenta')) verify_color(Color('magenta') - Blue(1), Color('red')) verify_color(Color('magenta') - Hue(0), Color('magenta')) verify_color(Color('magenta') - Lightness(0.5), Color('black')) verify_color(Color('magenta') - Saturation(1), Color(0.5, 0.5, 0.5)) verify_color(Color('magenta') - Luma(1), Color('black')) verify_color(Red(1) - Color('magenta'), Color(0, 0, 0)) verify_color(Green(1) - Color('magenta'), Color(0, 1, 0)) verify_color(Blue(1) - Color('magenta'), Color(0, 0, 0)) # pylint: disable=expression-not-assigned with pytest.raises(TypeError): Color('magenta') - 1 with pytest.raises(TypeError): 1 - Color('magenta') def test_color_mul(): verify_color(Color('magenta') * Color('blue'), Color('blue')) verify_color(Color('magenta') * Color('white'), Color('magenta')) verify_color(Color('magenta') * Red(0.5), Color(0.5, 0, 1)) verify_color(Color('magenta') * Green(0.5), Color('magenta')) verify_color(Color('magenta') * Blue(0.5), Color(1, 0, 0.5)) verify_color(Color('magenta') * Hue(0), Color('red')) verify_color(Color('magenta') * Lightness(0.5), Color(0.5, 0.0, 0.5)) verify_color(Color('magenta') * Saturation(0), Color(0.5, 0.5, 0.5)) verify_color(Color('magenta') * Luma(1), Color('magenta')) verify_color(Red(0.5) * Color('magenta'), Color(0.5, 0, 1)) # pylint: disable=expression-not-assigned with pytest.raises(TypeError): Color('magenta') * 1 with pytest.raises(TypeError): 1 * Color('magenta') def test_color_repr(): save_style = Color.repr_style try: Color.repr_style = 'default' assert repr(Color('red')) == "" % '#ff0000' Color.repr_style = 'html' assert repr(Color('red')) == "Color(%r)" % '#ff0000' Color.repr_style = 'rgb' assert repr(Color('red')) == "Color(1, 0, 0)" Color.repr_style = 'term16m' assert repr(Color('red')) == "" Color.repr_style = 'term256' assert repr(Color('red')) == "" Color.repr_style = 'foo' with pytest.raises(ValueError): repr(Color('red')) finally: Color.repr_style = save_style def test_color_str(): assert str(Color('black')) == '#000000' assert str(Color('red')) == '#ff0000' assert str(Color('white')) == '#ffffff' def test_color_html(): assert Color('black').html == '#000000' assert Color('red').html == '#ff0000' assert Color('white').html == '#ffffff' def test_color_rgb(): assert Color('black').rgb == RGB(0, 0, 0) assert Color('red').rgb == RGB(1, 0, 0) assert Color('white').rgb == RGB(1, 1, 1) def test_color_rgb565(): assert Color('black').rgb565 == 0 assert Color('red').rgb565 == 0x1f << 11 assert Color('white').rgb565 == 0xffff def test_color_rgb_bytes(): assert Color('black').rgb_bytes == RGB(0, 0, 0) assert Color('red').rgb_bytes == RGB(255, 0, 0) assert Color('white').rgb_bytes == RGB(255, 255, 255) def test_color_yuv(): verify_color(Color('black').yuv, YUV(0, 0, 0)) verify_color(Color('white').yuv, YUV(1, 0, 0)) verify_color(Color('red').yuv, YUV(y=0.299, u=-0.14713, v=0.615), abs_tol=1e-5) def test_color_yuv_bytes(): verify_color(Color('black').yuv_bytes, YUV(16, 128, 128)) verify_color(Color('white').yuv_bytes, YUV(235, 128, 128)) verify_color(Color('red').yuv_bytes, YUV(82, 90, 240)) def test_color_yiq(): verify_color(Color('black').yiq, YIQ(0, 0, 0)) verify_color(Color('white').yiq, YIQ(1, 0, 0)) verify_color(Color('red').yiq, YIQ(0.3, 0.599, 0.213)) def test_color_hls(): assert Color('black').hls == HLS(0, 0, 0) assert Color('white').hls == HLS(0, 1, 0) assert Color('red').hls == HLS(0, 0.5, 1) def test_color_hsv(): assert Color('black').hsv == HSV(0, 0, 0) assert Color('white').hsv == HSV(0, 0, 1) assert Color('red').hsv == HSV(0, 1, 1) def test_color_cmy(): assert Color('black').cmy == CMY(1, 1, 1) assert Color('white').cmy == CMY(0, 0, 0) assert Color('red').cmy == CMY(0, 1, 1) def test_color_cmyk(): assert Color('black').cmyk == CMYK(0, 0, 0, 1) assert Color('white').cmyk == CMYK(0, 0, 0, 0) assert Color('red').cmyk == CMYK(0, 1, 1, 0) def test_color_xyz(): verify_color(Color('black').xyz, XYZ(0, 0, 0)) verify_color(Color('white').xyz, XYZ(0.95047, 1, 1.08883), abs_tol=1e-5) verify_color(Color('red').xyz, XYZ(0.41246, 0.21267, 0.01933), abs_tol=1e-5) def test_color_lab(): verify_color(Color('black').lab, Lab(0, 0, 0)) verify_color(Color('white').lab, Lab(100, 0, 0), abs_tol=1e-4) verify_color(Color('red').lab, Lab(53.24079, 80.09246, 67.2032), abs_tol=1e-4) def test_color_luv(): verify_color(Color('black').luv, Luv(0, 0, 0)) verify_color(Color('white').luv, Luv(100, 0, 0), abs_tol=1e-4) verify_color(Color('red').luv, Luv(53.24079, 175.01503, 37.75643), abs_tol=1e-4) def test_color_attr(): assert Color('red').hue == Hue(0) assert Color('red').lightness == Lightness(0.5) assert Color('red').saturation == Saturation(1) assert Color('red').luma == Luma(0.299) def test_color_diff(): assert Color('black').difference(Color('black')) == 0.0 assert Color('white').difference(Color('black')) == sqrt(3) assert Color('red').difference(Color('black')) == 1.0 assert Color('black').difference(Color('black'), 'cie1976') == 0.0 assert Color('black').difference(Color('black'), 'ciede2000') == 0.0 with pytest.raises(ValueError): Color('red').difference(Color('black'), method='foo') with pytest.raises(ValueError): Color('red').difference(Color('black'), method=b'foo') def test_color_format(): black = Color('black') red = Color('red') blue = Color('#004') assert '{:0}{:0}{:0}'.format(black, red, blue) == '\x1b[0m' * 3 assert '{}{}{}'.format(black, red, blue) == '\x1b[30m\x1b[1;31m\x1b[34m' assert '{:b}{:b8}{:b8}'.format(black, red, blue) == '\x1b[40m\x1b[41m\x1b[44m' assert '{0:256}{0:b256}'.format(black) == '\x1b[38;5;0m\x1b[48;5;0m' assert '{0:256}{0:b256}'.format(red) == '\x1b[38;5;9m\x1b[48;5;9m' assert '{0:256}{0:b256}'.format(blue) == '\x1b[38;5;17m\x1b[48;5;17m' assert '{0:16m}{0:b16m}'.format(black) == '\x1b[38;2;0;0;0m\x1b[48;2;0;0;0m' assert '{0:16m}{0:b16m}'.format(red) == '\x1b[38;2;255;0;0m\x1b[48;2;255;0;0m' assert '{0:16m}{0:b16m}'.format(blue) == '\x1b[38;2;0;0;68m\x1b[48;2;0;0;68m' assert '{0:html}{1:html}'.format(red, blue) == '#ff0000#000044' assert '{0:css}'.format(red) == 'rgb(255, 0, 0)' assert '{0:cssrgb}'.format(blue) == 'rgb(0, 0, 68)' assert '{0:csshsl}'.format(blue) == 'hsl(240deg, 100%, 13.3333%)' with pytest.raises(ValueError): '{:foo}'.format(black) def test_color_gradient(): black = Color('black') white = Color('white') red = Color('red') assert list(black.gradient(white, 2)) == [black, white] assert list(black.gradient(white, 5)) == [ black, Color(0.25, 0.25, 0.25), Color(0.5, 0.5, 0.5), Color(0.75, 0.75, 0.75), white, ] assert list(white.gradient(red, 5)) == [ white, Color(1, 0.75, 0.75), Color(1, 0.5, 0.5), Color(1, 0.25, 0.25), red, ] with pytest.raises(ValueError): list(black.gradient(white, 1)) colorzero-release-2.0/tests/test_conversions.py000066400000000000000000000302731402377017200221420ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2018 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause "Tests for the colorzero.conversions module" from __future__ import ( unicode_literals, print_function, division, absolute_import, ) try: from math import isclose except ImportError: from compat import isclose # pylint: disable=wrong-import-order,import-error,missing-docstring import pytest from colorzero import conversions as cv def frange(start, stop, step): i = 0 while True: value = start + (i * step) if value > stop: break yield value i += 1 def verify_floats(color1, color2, abs_tol=1e-7): for elem1, elem2 in zip(color1, color2): assert isclose(elem1, elem2, abs_tol=abs_tol) def verify_ints(color1, color2, abs_tol=1): for elem1, elem2 in zip(color1, color2): assert abs(elem1 - elem2) <= abs_tol # Stop pylint warning about pytest fixtures # pylint: disable=redefined-outer-name @pytest.fixture(params=( (r, g, b) for r in frange(0.0, 1.0, 0.2) for g in frange(0.0, 1.0, 0.2) for b in frange(0.0, 1.0, 0.2) )) def rgb(request): return request.param @pytest.fixture(params=( (r, g, b) for r in range(0, 255, 64) for g in range(0, 255, 64) for b in range(0, 255, 64) )) def rgb_bytes(request): return request.param @pytest.fixture() def html7(request, rgb_bytes): # pylint: disable=unused-argument return '#%02x%02x%02x' % rgb_bytes @pytest.fixture(params=( (r, g, b) for r in range(0, 0xf, 0x4) for g in range(0, 0xf, 0x4) for b in range(0, 0xf, 0x4) )) def html3(request): return '#%x%x%x' % request.param def test_yiq_roundtrip(rgb): verify_floats(cv.yiq_to_rgb(*cv.rgb_to_yiq(*rgb)), rgb) def test_yiq_knowns(): # pylint: disable=bad-whitespace values = [ # rgb, yiq ((0.0, 0.0, 0.0), (0.0, 0.0, 0.0)), # black ((0.0, 0.0, 1.0), (0.11, -0.3217, 0.3121)), # blue ((0.0, 1.0, 0.0), (0.59, -0.2773, -0.5251)), # green ((0.0, 1.0, 1.0), (0.7, -0.599, -0.213)), # cyan ((1.0, 0.0, 0.0), (0.3, 0.599, 0.213)), # red ((1.0, 0.0, 1.0), (0.41, 0.2773, 0.5251)), # purple ((1.0, 1.0, 0.0), (0.89, 0.3217, -0.3121)), # yellow ((1.0, 1.0, 1.0), (1.0, 0.0, 0.0)), # white ((0.5, 0.5, 0.5), (0.5, 0.0, 0.0)), # grey ] for rgb, yiq in values: verify_floats(cv.rgb_to_yiq(*rgb), yiq) verify_floats(cv.yiq_to_rgb(*yiq), rgb) def test_hls_roundtrip(rgb): verify_floats(cv.hls_to_rgb(*cv.rgb_to_hls(*rgb)), rgb) def test_hls_knowns(rgb): # pylint: disable=bad-whitespace values = [ # rgb, hls ((0.0, 0.0, 0.0), ( 0, 0.0, 0.0)), # black ((0.0, 0.0, 1.0), (4/6, 0.5, 1.0)), # blue ((0.0, 1.0, 0.0), (2/6, 0.5, 1.0)), # green ((0.0, 1.0, 1.0), (3/6, 0.5, 1.0)), # cyan ((1.0, 0.0, 0.0), ( 0, 0.5, 1.0)), # red ((1.0, 0.0, 1.0), (5/6, 0.5, 1.0)), # purple ((1.0, 1.0, 0.0), (1/6, 0.5, 1.0)), # yellow ((1.0, 1.0, 1.0), ( 0, 1.0, 0.0)), # white ((0.5, 0.5, 0.5), ( 0, 0.5, 0.0)), # grey ] for rgb, hls in values: verify_floats(cv.rgb_to_hls(*rgb), hls) verify_floats(cv.hls_to_rgb(*hls), rgb) def test_hsv_roundtrip(rgb): verify_floats(cv.hsv_to_rgb(*cv.rgb_to_hsv(*rgb)), rgb) def test_hsv_knowns(): # pylint: disable=bad-whitespace values = [ # rgb, hsv ((0.0, 0.0, 0.0), ( 0, 0.0, 0.0)), # black ((0.0, 0.0, 1.0), (4/6, 1.0, 1.0)), # blue ((0.0, 1.0, 0.0), (2/6, 1.0, 1.0)), # green ((0.0, 1.0, 1.0), (3/6, 1.0, 1.0)), # cyan ((1.0, 0.0, 0.0), ( 0, 1.0, 1.0)), # red ((1.0, 0.0, 1.0), (5/6, 1.0, 1.0)), # purple ((1.0, 1.0, 0.0), (1/6, 1.0, 1.0)), # yellow ((1.0, 1.0, 1.0), ( 0, 0.0, 1.0)), # white ((0.5, 0.5, 0.5), ( 0, 0.0, 0.5)), # grey ] for rgb, hsv in values: verify_floats(cv.rgb_to_hsv(*rgb), hsv) verify_floats(cv.hsv_to_rgb(*hsv), rgb) def test_rgb_bytes_roundtrip(rgb): verify_floats(cv.rgb_bytes_to_rgb(*cv.rgb_to_rgb_bytes(*rgb)), rgb) def test_rgb_bytes_known(): # pylint: disable=bad-whitespace values = [ # rgb, bytes ((0.0, 0.0, 0.0), (0, 0, 0)), # black ((0.0, 0.0, 1.0), (0, 0, 255)), # blue ((0.0, 1.0, 0.0), (0, 255, 0)), # green ((0.0, 1.0, 1.0), (0, 255, 255)), # cyan ((1.0, 0.0, 0.0), (255, 0, 0)), # red ((1.0, 0.0, 1.0), (255, 0, 255)), # purple ((1.0, 1.0, 0.0), (255, 255, 0)), # yellow ((1.0, 1.0, 1.0), (255, 255, 255)), # white ((0.4, 0.4, 0.4), (102, 102, 102)), # grey ] for rgb, b in values: assert cv.rgb_to_rgb_bytes(*rgb) == b assert cv.rgb_bytes_to_rgb(*b) == rgb def test_rgb24_roundtrip(rgb_bytes): assert cv.rgb24_to_rgb_bytes(cv.rgb_bytes_to_rgb24(*rgb_bytes)) == rgb_bytes def test_rgb24_known(): # pylint: disable=bad-whitespace values = [ # rgb, rgb24 ((0, 0, 0), 0), # black ((0, 0, 255), 0xff0000), # blue ((0, 255, 0), 0xff00), # green ((0, 255, 255), 0xffff00), # cyan ((255, 0, 0), 0xff), # red ((255, 0, 255), 0xff00ff), # purple ((255, 255, 0), 0xffff), # yellow ((255, 255, 255), 0xffffff), # white ((102, 102, 102), 0x666666), # grey ] for rgb, rgb24 in values: assert cv.rgb24_to_rgb_bytes(rgb24) == rgb assert cv.rgb_bytes_to_rgb24(*rgb) == rgb24 def test_rgb_html7_roundtrip(html7): assert cv.rgb_bytes_to_html(*cv.html_to_rgb_bytes(html7)) == html7 def test_rgb_html3_roundtrip(html3): v = '#' + html3[1] + html3[1] + html3[2] + html3[2] + html3[3] + html3[3] assert cv.rgb_bytes_to_html(*cv.html_to_rgb_bytes(html3)) == v def test_html_known(): # pylint: disable=bad-whitespace values = [ # rgb, html ((0, 0, 0), '#000000'), # black ((0, 0, 255), '#0000ff'), # blue ((0, 255, 0), '#00ff00'), # green ((0, 255, 255), '#00ffff'), # cyan ((255, 0, 0), '#ff0000'), # red ((255, 0, 255), '#ff00ff'), # purple ((255, 255, 0), '#ffff00'), # yellow ((255, 255, 255), '#ffffff'), # white ((102, 102, 102), '#666666'), # grey ] for rgb, rgb24 in values: assert cv.html_to_rgb_bytes(rgb24) == rgb assert cv.rgb_bytes_to_html(*rgb) == rgb24 def test_html_bad(): with pytest.raises(ValueError): cv.html_to_rgb_bytes('#1') with pytest.raises(ValueError): cv.html_to_rgb_bytes('#11223344') def test_cmy_roundtrip(rgb): verify_floats(cv.cmy_to_rgb(*cv.rgb_to_cmy(*rgb)), rgb) def test_cmy_known(): # pylint: disable=bad-whitespace values = [ # rgb, cmy ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), # black ((0.0, 0.0, 1.0), (1.0, 1.0, 0.0)), # blue ((0.0, 1.0, 0.0), (1.0, 0.0, 1.0)), # green ((0.0, 1.0, 1.0), (1.0, 0.0, 0.0)), # cyan ((1.0, 0.0, 0.0), (0.0, 1.0, 1.0)), # red ((1.0, 0.0, 1.0), (0.0, 1.0, 0.0)), # purple ((1.0, 1.0, 0.0), (0.0, 0.0, 1.0)), # yellow ((1.0, 1.0, 1.0), (0.0, 0.0, 0.0)), # white ((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), # grey ] for rgb, cmy in values: verify_floats(cv.rgb_to_cmy(*rgb), cmy) verify_floats(cv.cmy_to_rgb(*cmy), rgb) def test_cmyk_roundtrip(rgb): verify_floats( cv.cmy_to_rgb( *cv.cmyk_to_cmy( *cv.cmy_to_cmyk( *cv.rgb_to_cmy(*rgb)))), rgb) def test_cmyk_known(): # pylint: disable=bad-whitespace values = [ # rgb, cmyk ((0.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0)), # black ((0.0, 0.0, 1.0), (1.0, 1.0, 0.0, 0.0)), # blue ((0.0, 1.0, 0.0), (1.0, 0.0, 1.0, 0.0)), # green ((0.0, 1.0, 1.0), (1.0, 0.0, 0.0, 0.0)), # cyan ((1.0, 0.0, 0.0), (0.0, 1.0, 1.0, 0.0)), # red ((1.0, 0.0, 1.0), (0.0, 1.0, 0.0, 0.0)), # purple ((1.0, 1.0, 0.0), (0.0, 0.0, 1.0, 0.0)), # yellow ((1.0, 1.0, 1.0), (0.0, 0.0, 0.0, 0.0)), # white ((0.5, 0.5, 0.5), (0.0, 0.0, 0.0, 0.5)), # grey ] for rgb, cmyk in values: verify_floats(cv.cmy_to_cmyk(*cv.rgb_to_cmy(*rgb)), cmyk) verify_floats(cv.cmy_to_rgb(*cv.cmyk_to_cmy(*cmyk)), rgb) def test_yuv_roundtrip(rgb): verify_floats(cv.yuv_to_rgb(*cv.rgb_to_yuv(*rgb)), rgb) def test_yuv_knowns(): # pylint: disable=bad-whitespace values = [ # rgb, yuv ((0.0, 0.0, 0.0), (0.0, 0.0, 0.0)), # black ((0.0, 0.0, 1.0), (0.114, 0.436, -0.10001)), # blue ((0.0, 1.0, 0.0), (0.587, -0.28886, -0.51498)), # green ((0.0, 1.0, 1.0), (0.701, 0.14714, -0.615)), # cyan ((1.0, 0.0, 0.0), (0.299, -0.14714, 0.615)), # red ((1.0, 0.0, 1.0), (0.413, 0.28886, 0.51498)), # purple ((1.0, 1.0, 0.0), (0.886, -0.436, 0.10001)), # yellow ((1.0, 1.0, 1.0), (1.0, 0.0, 0.0)), # white ((0.5, 0.5, 0.5), (0.5, 0.0, 0.0)), # grey ] for rgb, yuv in values: verify_floats(cv.rgb_to_yuv(*rgb), yuv, abs_tol=1e-5) verify_floats(cv.yuv_to_rgb(*yuv), rgb, abs_tol=1e-5) def test_yuv_bytes_roundtrip(rgb_bytes): # Tolerance of 2 here to allow for accumulated error in the roundtrip verify_ints( cv.yuv_bytes_to_rgb_bytes(*cv.rgb_bytes_to_yuv_bytes(*rgb_bytes)), rgb_bytes, abs_tol=2) def test_yuv_bytes_knowns(): # pylint: disable=bad-whitespace values = [ # rgb, yuv ((0, 0, 0), (16, 128, 128)), # black ((0, 0, 255), (41, 240, 110)), # blue ((0, 255, 0), (144, 54, 34)), # green ((0, 255, 255), (169, 166, 16)), # cyan ((255, 0, 0), (82, 90, 240)), # red ((255, 0, 255), (107, 202, 222)), # purple ((255, 255, 0), (210, 16, 146)), # yellow ((255, 255, 255), (235, 128, 128)), # white ((102, 102, 102), (104, 128, 128)), # grey ] for rgb, yuv in values: verify_ints(cv.rgb_bytes_to_yuv_bytes(*rgb), yuv) verify_ints(cv.yuv_bytes_to_rgb_bytes(*yuv), rgb) def test_yuv_coefficients(): with pytest.raises(TypeError): cv.YUVCoefficients() def test_xyz_roundtrip(rgb): # XYZ is a more complex conversion and tends to lose more precision during # the round-trip verify_floats(cv.xyz_to_rgb(*cv.rgb_to_xyz(*rgb)), rgb, abs_tol=1e-5) def test_xyz_known(): # pylint: disable=bad-whitespace values = [ # rgb, xyz ((0.0, 0.0, 0.0), (0.0, 0.0, 0.0)), # black ((0.0, 0.0, 1.0), (0.18043, 0.07217, 0.95030)), # blue ((0.0, 1.0, 0.0), (0.35758, 0.71515, 0.11919)), # green ((0.0, 1.0, 1.0), (0.53801, 0.78733, 1.06950)), # cyan ((1.0, 0.0, 0.0), (0.41246, 0.21267, 0.01933)), # red ((1.0, 0.0, 1.0), (0.59289, 0.28485, 0.96964)), # purple ((1.0, 1.0, 0.0), (0.77003, 0.92782, 0.13852)), # yellow ((1.0, 1.0, 1.0), (0.95047, 1.00000, 1.08883)), # white ((0.5, 0.5, 0.5), (0.20344, 0.21404, 0.23305)), # grey ] for rgb, xyz in values: verify_floats(cv.rgb_to_xyz(*rgb), xyz, abs_tol=1e-5) # The XYZ values above are very rough approximations, hence the huge # tolerance here verify_floats(cv.xyz_to_rgb(*xyz), rgb, abs_tol=1e-3) def test_lab_roundtrip(rgb): verify_floats( cv.xyz_to_rgb( *cv.lab_to_xyz( *cv.xyz_to_lab( *cv.rgb_to_xyz(*rgb)))), rgb, abs_tol=1e-5) def test_luv_roundtrip(rgb): verify_floats( cv.xyz_to_rgb( *cv.luv_to_xyz( *cv.xyz_to_luv( *cv.rgb_to_xyz(*rgb)))), rgb, abs_tol=1e-5) def test_bad_html(): with pytest.raises(ValueError): cv.html_to_rgb_bytes('foo') with pytest.raises(ValueError): cv.html_to_rgb_bytes('#foo') def test_bad_name(): with pytest.raises(ValueError): cv.name_to_html('foo') colorzero-release-2.0/tests/test_deltae.py000066400000000000000000000253051402377017200210300ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2018 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause "Tests for the colorzero.deltae module" from __future__ import ( unicode_literals, print_function, division, absolute_import, ) try: from math import isclose except ImportError: from compat import isclose # pylint: disable=wrong-import-order,import-error,missing-docstring from colorzero import Lab, deltae as de def test_cie1976_known(): # XXX Test values from current implementation; if anyone can point me at # a source of "known" test values for CIE1976 I'd be most grateful! # pylint: disable=bad-whitespace values = [ # color1, color2, delta-e (graphics), delta-e (textiles) (Lab(50.0000, 2.6772, -79.7751), Lab(50.0000, 0.0000, -82.7485), 4.0011), (Lab(50.0000, 3.1571, -77.2803), Lab(50.0000, 0.0000, -82.7485), 6.3142), (Lab(50.0000, 2.8361, -74.0200), Lab(50.0000, 0.0000, -82.7485), 9.1777), (Lab(50.0000, -1.3802, -84.2814), Lab(50.0000, 0.0000, -82.7485), 2.0627), (Lab(50.0000, -1.1848, -84.8006), Lab(50.0000, 0.0000, -82.7485), 2.3696), (Lab(50.0000, -0.9009, -85.5211), Lab(50.0000, 0.0000, -82.7485), 2.9153), (Lab(50.0000, 0.0000, 0.0000), Lab(50.0000, -1.0000, 2.0000), 2.2361), (Lab(50.0000, -1.0000, 2.0000), Lab(50.0000, 0.0000, 0.0000), 2.2361), (Lab(50.0000, 2.4900, -0.0010), Lab(50.0000, -2.4900, 0.0009), 4.9800), (Lab(50.0000, 2.4900, -0.0010), Lab(50.0000, -2.4900, 0.0010), 4.9800), (Lab(50.0000, 2.4900, -0.0010), Lab(50.0000, -2.4900, 0.0011), 4.9800), (Lab(50.0000, 2.4900, -0.0010), Lab(50.0000, -2.4900, 0.0012), 4.9800), (Lab(50.0000, -0.0010, 2.4900), Lab(50.0000, 0.0009, -2.4900), 4.9800), (Lab(50.0000, -0.0010, 2.4900), Lab(50.0000, 0.0010, -2.4900), 4.9800), (Lab(50.0000, -0.0010, 2.4900), Lab(50.0000, 0.0011, -2.4900), 4.9800), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 0.0000, -2.5000), 3.5355), (Lab(50.0000, 2.5000, 0.0000), Lab(73.0000, 25.0000, -18.0000), 36.8680), (Lab(50.0000, 2.5000, 0.0000), Lab(61.0000, -5.0000, 29.0000), 31.9100), (Lab(50.0000, 2.5000, 0.0000), Lab(56.0000, -27.0000, -3.0000), 30.2531), (Lab(50.0000, 2.5000, 0.0000), Lab(58.0000, 24.0000, 15.0000), 27.4089), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 3.1736, 0.5854), 0.8924), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 3.2972, 0.0000), 0.7972), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 1.8634, 0.5757), 0.8583), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 3.2592, 0.3350), 0.8298), (Lab(60.2574, -34.0099, 36.2677), Lab(60.4626, -34.1751, 39.4387), 3.1819), (Lab(63.0109, -31.0961, -5.8663), Lab(62.8187, -29.7946, -4.0864), 2.2133), (Lab(61.2901, 3.7196, -5.3901), Lab(61.4292, 2.2480, -4.9620), 1.5389), (Lab(35.0831, -44.1164, 3.7933), Lab(35.0232, -40.0716, 1.5901), 4.6063), (Lab(22.7233, 20.0904, -46.6940), Lab(23.0331, 14.9730, -42.5619), 6.5847), (Lab(36.4612, 47.8580, 18.3852), Lab(36.2715, 50.5065, 21.2231), 3.8864), (Lab(90.8027, -2.0831, 1.4410), Lab(91.1528, -1.6435, 0.0447), 1.5051), (Lab(90.9257, -0.5406, -0.9208), Lab(88.6381, -0.8985, -0.7239), 2.3238), (Lab( 6.7747, -0.2908, -2.4247), Lab( 5.8714, -0.0985, -2.2286), 0.9441), (Lab( 2.0776, 0.0795, -1.1350), Lab( 0.9033, -0.0636, -0.5514), 1.3191), ] for color1, color2, diff in values: assert isclose(de.cie1976(color1, color2), diff, abs_tol=1e-4) def test_cie1994_known(): # XXX Test values from current implementation; if anyone can point me at # a source of "known" test values for CIE1994 I'd be most grateful! # pylint: disable=bad-whitespace values = [ # color1, color2, delta-e (graphics), delta-e (textiles) (Lab(50.0000, 2.6772, -79.7751), Lab(50.0000, 0.0000, -82.7485), 1.3950, 1.4230), (Lab(50.0000, 3.1571, -77.2803), Lab(50.0000, 0.0000, -82.7485), 1.9341, 1.9427), (Lab(50.0000, 2.8361, -74.0200), Lab(50.0000, 0.0000, -82.7485), 2.4543, 2.4066), (Lab(50.0000, -1.3802, -84.2814), Lab(50.0000, 0.0000, -82.7485), 0.6845, 0.6980), (Lab(50.0000, -1.1848, -84.8006), Lab(50.0000, 0.0000, -82.7485), 0.6696, 0.6719), (Lab(50.0000, -0.9009, -85.5211), Lab(50.0000, 0.0000, -82.7485), 0.6919, 0.6772), (Lab(50.0000, 0.0000, 0.0000), Lab(50.0000, -1.0000, 2.0000), 2.2361, 2.2361), (Lab(50.0000, -1.0000, 2.0000), Lab(50.0000, 0.0000, 0.0000), 2.0316, 2.0193), (Lab(50.0000, 2.4900, -0.0010), Lab(50.0000, -2.4900, 0.0009), 4.8007, 4.8122), (Lab(50.0000, 2.4900, -0.0010), Lab(50.0000, -2.4900, 0.0010), 4.8007, 4.8122), (Lab(50.0000, 2.4900, -0.0010), Lab(50.0000, -2.4900, 0.0011), 4.8007, 4.8122), (Lab(50.0000, 2.4900, -0.0010), Lab(50.0000, -2.4900, 0.0012), 4.8007, 4.8122), (Lab(50.0000, -0.0010, 2.4900), Lab(50.0000, 0.0009, -2.4900), 4.8007, 4.8122), (Lab(50.0000, -0.0010, 2.4900), Lab(50.0000, 0.0010, -2.4900), 4.8007, 4.8122), (Lab(50.0000, -0.0010, 2.4900), Lab(50.0000, 0.0011, -2.4900), 4.8007, 4.8122), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 0.0000, -2.5000), 3.4077, 3.4160), (Lab(50.0000, 2.5000, 0.0000), Lab(73.0000, 25.0000, -18.0000), 34.6892, 28.2503), (Lab(50.0000, 2.5000, 0.0000), Lab(61.0000, -5.0000, 29.0000), 29.4414, 27.7308), (Lab(50.0000, 2.5000, 0.0000), Lab(56.0000, -27.0000, -3.0000), 27.9141, 27.3286), (Lab(50.0000, 2.5000, 0.0000), Lab(58.0000, 24.0000, 15.0000), 24.9377, 23.8076), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 3.1736, 0.5854), 0.8221, 0.8194), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 3.2972, 0.0000), 0.7166, 0.7118), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 1.8634, 0.5757), 0.8049, 0.8041), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 3.2592, 0.3350), 0.7528, 0.7488), (Lab(60.2574, -34.0099, 36.2677), Lab(60.4626, -34.1751, 39.4387), 1.3910, 1.3897), (Lab(63.0109, -31.0961, -5.8663), Lab(62.8187, -29.7946, -4.0864), 1.2481, 1.2441), (Lab(61.2901, 3.7196, -5.3901), Lab(61.4292, 2.2480, -4.9620), 1.2980, 1.2884), (Lab(35.0831, -44.1164, 3.7933), Lab(35.0232, -40.0716, 1.5901), 1.8205, 1.7958), (Lab(22.7233, 20.0904, -46.6940), Lab(23.0331, 14.9730, -42.5619), 2.5561, 2.5310), (Lab(36.4612, 47.8580, 18.3852), Lab(36.2715, 50.5065, 21.2231), 1.4249, 1.3991), (Lab(90.8027, -2.0831, 1.4410), Lab(91.1528, -1.6435, 0.0447), 1.4195, 1.3858), (Lab(90.9257, -0.5406, -0.9208), Lab(88.6381, -0.8985, -0.7239), 2.3226, 1.2123), (Lab( 6.7747, -0.2908, -2.4247), Lab( 5.8714, -0.0985, -2.2286), 0.9385, 0.5182), (Lab( 2.0776, 0.0795, -1.1350), Lab( 0.9033, -0.0636, -0.5514), 1.3065, 0.8191), ] for color1, color2, diffg, difft in values: assert isclose(de.cie1994g(color1, color2), diffg, abs_tol=1e-4) assert isclose(de.cie1994t(color1, color2), difft, abs_tol=1e-4) def test_ciede2000_known(): # Test values from Sharma 2005: # http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf # pylint: disable=bad-whitespace values = [ # color1, color2, delta-e (Lab(50.0000, 2.6772, -79.7751), Lab(50.0000, 0.0000, -82.7485), 2.0425), (Lab(50.0000, 3.1571, -77.2803), Lab(50.0000, 0.0000, -82.7485), 2.8615), (Lab(50.0000, 2.8361, -74.0200), Lab(50.0000, 0.0000, -82.7485), 3.4412), (Lab(50.0000, -1.3802, -84.2814), Lab(50.0000, 0.0000, -82.7485), 1.0000), (Lab(50.0000, -1.1848, -84.8006), Lab(50.0000, 0.0000, -82.7485), 1.0000), (Lab(50.0000, -0.9009, -85.5211), Lab(50.0000, 0.0000, -82.7485), 1.0000), (Lab(50.0000, 0.0000, 0.0000), Lab(50.0000, -1.0000, 2.0000), 2.3669), (Lab(50.0000, -1.0000, 2.0000), Lab(50.0000, 0.0000, 0.0000), 2.3669), (Lab(50.0000, 2.4900, -0.0010), Lab(50.0000, -2.4900, 0.0009), 7.1792), (Lab(50.0000, 2.4900, -0.0010), Lab(50.0000, -2.4900, 0.0010), 7.1792), (Lab(50.0000, 2.4900, -0.0010), Lab(50.0000, -2.4900, 0.0011), 7.2195), (Lab(50.0000, 2.4900, -0.0010), Lab(50.0000, -2.4900, 0.0012), 7.2195), (Lab(50.0000, -0.0010, 2.4900), Lab(50.0000, 0.0009, -2.4900), 4.8045), (Lab(50.0000, -0.0010, 2.4900), Lab(50.0000, 0.0010, -2.4900), 4.8045), (Lab(50.0000, -0.0010, 2.4900), Lab(50.0000, 0.0011, -2.4900), 4.7461), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 0.0000, -2.5000), 4.3065), (Lab(50.0000, 2.5000, 0.0000), Lab(73.0000, 25.0000, -18.0000), 27.1492), (Lab(50.0000, 2.5000, 0.0000), Lab(61.0000, -5.0000, 29.0000), 22.8977), (Lab(50.0000, 2.5000, 0.0000), Lab(56.0000, -27.0000, -3.0000), 31.9030), (Lab(50.0000, 2.5000, 0.0000), Lab(58.0000, 24.0000, 15.0000), 19.4535), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 3.1736, 0.5854), 1.0000), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 3.2972, 0.0000), 1.0000), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 1.8634, 0.5757), 1.0000), (Lab(50.0000, 2.5000, 0.0000), Lab(50.0000, 3.2592, 0.3350), 1.0000), (Lab(60.2574, -34.0099, 36.2677), Lab(60.4626, -34.1751, 39.4387), 1.2644), (Lab(63.0109, -31.0961, -5.8663), Lab(62.8187, -29.7946, -4.0864), 1.2630), (Lab(61.2901, 3.7196, -5.3901), Lab(61.4292, 2.2480, -4.9620), 1.8731), (Lab(35.0831, -44.1164, 3.7933), Lab(35.0232, -40.0716, 1.5901), 1.8645), (Lab(22.7233, 20.0904, -46.6940), Lab(23.0331, 14.9730, -42.5619), 2.0373), (Lab(36.4612, 47.8580, 18.3852), Lab(36.2715, 50.5065, 21.2231), 1.4146), (Lab(90.8027, -2.0831, 1.4410), Lab(91.1528, -1.6435, 0.0447), 1.4441), (Lab(90.9257, -0.5406, -0.9208), Lab(88.6381, -0.8985, -0.7239), 1.5381), (Lab( 6.7747, -0.2908, -2.4247), Lab( 5.8714, -0.0985, -2.2286), 0.6377), (Lab( 2.0776, 0.0795, -1.1350), Lab( 0.9033, -0.0636, -0.5514), 0.9082), ] for color1, color2, diff in values: assert isclose(de.ciede2000(color1, color2), diff, abs_tol=1e-4) colorzero-release-2.0/tests/test_easings.py000066400000000000000000000015161402377017200212210ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2018 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause "Tests for the colorzero.easings module" from __future__ import ( unicode_literals, print_function, division, absolute_import, ) import pytest from colorzero.easings import * def test_linear(): assert list(linear(2)) == [0, 1] assert list(linear(5)) == [0, 1/4, 1/2, 3/4, 1] def test_ease_in(): assert list(ease_in(2)) == [0, 1] assert list(ease_in(5)) == [0, 1/16, 4/16, 9/16, 1] def test_ease_out(): assert list(ease_out(2)) == [0, 1] assert list(ease_out(5)) == [0, 7/16, 12/16, 15/16, 1] def test_ease_in_out(): assert list(ease_in_out(2)) == [0, 1] assert list(ease_in_out(5)) == [0, 2/16, 8/16, 14/16, 1] colorzero-release-2.0/tests/test_types.py000066400000000000000000000046501402377017200207360ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # The colorzero color library # # Copyright (c) 2018 Dave Jones # # SPDX-License-Identifier: BSD-3-Clause "Tests for the colorzero.types module." from __future__ import ( unicode_literals, print_function, division, absolute_import, ) # pylint: disable=import-error,missing-docstring import pytest from colorzero import RGB, YUV, HLS, HSV, CMY, CMYK def test_rgb(): assert RGB(1, 1, 1)._replace(r=0) == RGB(0, 1, 1) with pytest.raises(TypeError): RGB(1, 1) with pytest.raises(ValueError): RGB(1, 1, 1)._replace(foo=1) v = RGB(1, 0.5, 0) assert v.r == v.red == 1 assert v.g == v.green == 0.5 assert v.b == v.blue == 0 assert v.__getnewargs__() == (1, 0.5, 0) assert repr(v) == 'RGB(r=1, g=0.5, b=0)' def test_hls(): assert HLS(1, 1, 1)._replace(h=0) == HLS(0, 1, 1) with pytest.raises(TypeError): HLS(1, 1) with pytest.raises(ValueError): HLS(1, 1, 1)._replace(foo=1) v = HLS(0, 0.5, 1) assert v.h == v.hue == 0 assert v.l == v.lightness == 0.5 assert v.s == v.saturation == 1 assert v.__getnewargs__() == (0, 0.5, 1) assert v._asdict() == {'h': 0, 'l': 0.5, 's': 1} assert repr(v) == 'HLS(h=0, l=0.5, s=1)' def test_hsv(): assert HSV(1, 1, 1)._replace(h=0) == HSV(0, 1, 1) with pytest.raises(TypeError): HSV(1, 1) with pytest.raises(ValueError): HSV(1, 1, 1)._replace(foo=1) v = HSV(0, 0.5, 1) assert v.h == v.hue == 0 assert v.s == v.saturation == 0.5 assert v.v == v.value == 1 assert v.__getnewargs__() == (0, 0.5, 1) assert v._asdict() == {'h': 0, 's': 0.5, 'v': 1} assert repr(v) == 'HSV(h=0, s=0.5, v=1)' def test_yuv(): assert YUV(1, 1, 1)._replace(y=0) == YUV(0, 1, 1) with pytest.raises(TypeError): YUV(1, 1) with pytest.raises(ValueError): YUV(1, 1, 1)._replace(foo=1) v = YUV(0, 0.5, 1) assert v.y == v.luma == 0 assert v.u == 0.5 assert v.v == 1 assert v.__getnewargs__() == (0, 0.5, 1) assert v._asdict() == {'y': 0, 'u': 0.5, 'v': 1} assert repr(v) == 'YUV(y=0, u=0.5, v=1)' def test_cmy(): v = CMY(0, 0.5, 1) assert v.cyan == 0 assert v.magenta == 0.5 assert v.yellow == 1 def test_cmyk(): v = CMYK(0, 0.5, 1, 0.2) assert v.cyan == 0 assert v.magenta == 0.5 assert v.yellow == 1 assert v.black == 0.2 colorzero-release-2.0/tox.ini000066400000000000000000000003131402377017200163220ustar00rootroot00000000000000[tox] envlist = {py35,py36,py37,py38,py39} [testenv] deps = .[test] usedevelop = true commands = make test whitelist_externals = make setenv = COVERAGE_FILE=.coverage.{envname} passenv = COVERAGE_*